diff --git a/source/compiler/qsc/src/interpret/circuit_tests.rs b/source/compiler/qsc/src/interpret/circuit_tests.rs index 1ab476f7df..0837f98bb7 100644 --- a/source/compiler/qsc/src/interpret/circuit_tests.rs +++ b/source/compiler/qsc/src/interpret/circuit_tests.rs @@ -697,23 +697,23 @@ fn operation_with_qubit_arrays() { ); expect![[r#" - q_0@test.qs:6:27 ─ H@test.qs:8:20 ─── M@test.qs:23:16 ─ - ╘═════════ - q_1@test.qs:6:27 ─ H@test.qs:8:20 ─── M@test.qs:23:16 ─ - ╘═════════ - q_2@test.qs:6:40 ─ X@test.qs:12:24 ──────────────────── - q_3@test.qs:6:40 ─ X@test.qs:12:24 ──────────────────── - q_4@test.qs:6:40 ─ X@test.qs:12:24 ──────────────────── - q_5@test.qs:6:40 ─ X@test.qs:12:24 ──────────────────── - q_6@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── - q_7@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── - q_8@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── - q_9@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── - q_10@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── - q_11@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── - q_12@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── - q_13@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── - q_14@test.qs:6:72 ─ X@test.qs:22:16 ──────────────────── + q_0@test.qs:6:27 ─ H@test.qs:8:20 ─── M@test.qs:23:16 ─ + ╘═════════ + q_1@test.qs:6:27 ─ H@test.qs:8:20 ─── M@test.qs:23:16 ─ + ╘═════════ + q_2@test.qs:6:40 ─ X@test.qs:12:24 ──────────────────── + q_3@test.qs:6:40 ─ X@test.qs:12:24 ──────────────────── + q_4@test.qs:6:40 ─ X@test.qs:12:24 ──────────────────── + q_5@test.qs:6:40 ─ X@test.qs:12:24 ──────────────────── + q_6@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── + q_7@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── + q_8@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── + q_9@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── + q_10@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── + q_11@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── + q_12@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── + q_13@test.qs:6:55 ─ Y@test.qs:18:28 ──────────────────── + q_14@test.qs:6:72 ─ X@test.qs:22:16 ──────────────────── "#]] .assert_eq(&circ); } @@ -909,13 +909,13 @@ fn operation_with_long_gates_properly_aligned() { ); expect![[r#" - q_0@test.qs:6:20 ─ H@test.qs:9:20 ───────────────────────────────────────────────────────────────────────── ● ────────────── M@test.qs:14:20 ─────────────────────────────────────────────────────────────────── ● ─────────────────────────── - │ ╘════════════════════════════════════════════════════════════════════════════╪════════════════════════════ - q_1@test.qs:7:20 ─ H@test.qs:10:20 ─────── X@test.qs:11:20 ─────── Ry(1.0000)@test.qs:12:20 ──────── X@test.qs:13:20 ─────────────────────────────────────────────────────── Rxx(1.0000)@test.qs:27:20 ──────────┼────────── M@test.qs:31:21 ─ - ┆ │ ╘═════════ - q_2@test.qs:16:20 ─ H@test.qs:18:20 ── Rx(1.0000)@test.qs:19:20 ──────── H@test.qs:20:20 ─────── Rx(1.0000)@test.qs:21:20 ─── H@test.qs:22:20 ── Rx(1.0000)@test.qs:23:20 ────────────────┆───────────────────────┼──────────────────────────── - q_3@test.qs:25:20 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Rxx(1.0000)@test.qs:27:20 ── X@test.qs:29:20 ── M@test.qs:31:28 ─ - ╘═════════ + q_0@test.qs:6:20 ─ H@test.qs:9:20 ───────────────────────────────────────────────────────────────────────── ● ────────────── M@test.qs:14:20 ─────────────────────────────────────────────────────────────────── ● ─────────────────────────── + │ ╘════════════════════════════════════════════════════════════════════════════╪════════════════════════════ + q_1@test.qs:7:20 ─ H@test.qs:10:20 ─────── X@test.qs:11:20 ─────── Ry(1.0000)@test.qs:12:20 ──────── X@test.qs:13:20 ─────────────────────────────────────────────────────── Rxx(1.0000)@test.qs:27:20 ──────────┼────────── M@test.qs:31:21 ─ + ┆ │ ╘═════════ + q_2@test.qs:16:20 ─ H@test.qs:18:20 ── Rx(1.0000)@test.qs:19:20 ──────── H@test.qs:20:20 ─────── Rx(1.0000)@test.qs:21:20 ─── H@test.qs:22:20 ── Rx(1.0000)@test.qs:23:20 ────────────────┆───────────────────────┼──────────────────────────── + q_3@test.qs:25:20 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Rxx(1.0000)@test.qs:27:20 ── X@test.qs:29:20 ── M@test.qs:31:28 ─ + ╘═════════ "#]] .assert_eq(&circ); } @@ -943,12 +943,12 @@ fn operation_with_subsequent_qubits_gets_horizontal_lines() { ); expect![[r#" - q_0@test.qs:6:20 ─ Rxx(1.0000)@test.qs:8:20 ── - ┆ - q_1@test.qs:7:20 ─ Rxx(1.0000)@test.qs:8:20 ── - q_2@test.qs:10:20 ─ Rxx(1.0000)@test.qs:12:20 ─ - ┆ - q_3@test.qs:11:20 ─ Rxx(1.0000)@test.qs:12:20 ─ + q_0@test.qs:6:20 ─ Rxx(1.0000)@test.qs:8:20 ── + ┆ + q_1@test.qs:7:20 ─ Rxx(1.0000)@test.qs:8:20 ── + q_2@test.qs:10:20 ─ Rxx(1.0000)@test.qs:12:20 ─ + ┆ + q_3@test.qs:11:20 ─ Rxx(1.0000)@test.qs:12:20 ─ "#]] .assert_eq(&circ); } @@ -1005,12 +1005,12 @@ fn operation_with_subsequent_qubits_no_added_rows() { ); expect![[r#" - q_0@test.qs:6:20 ─ Rxx(1.0000)@test.qs:8:20 ─── M@test.qs:14:21 ─ - ┆ ╘═════════ - q_1@test.qs:7:20 ─ Rxx(1.0000)@test.qs:8:20 ───────────────────── - q_2@test.qs:10:20 ─ Rxx(1.0000)@test.qs:12:20 ── M@test.qs:14:28 ─ - ┆ ╘═════════ - q_3@test.qs:11:20 ─ Rxx(1.0000)@test.qs:12:20 ──────────────────── + q_0@test.qs:6:20 ─ Rxx(1.0000)@test.qs:8:20 ─── M@test.qs:14:21 ─ + ┆ ╘═════════ + q_1@test.qs:7:20 ─ Rxx(1.0000)@test.qs:8:20 ───────────────────── + q_2@test.qs:10:20 ─ Rxx(1.0000)@test.qs:12:20 ── M@test.qs:14:28 ─ + ┆ ╘═════════ + q_3@test.qs:11:20 ─ Rxx(1.0000)@test.qs:12:20 ──────────────────── "#]] .assert_eq(&circ); } diff --git a/source/compiler/qsc_circuit/src/builder.rs b/source/compiler/qsc_circuit/src/builder.rs index 8aa0a116b5..1de42710c0 100644 --- a/source/compiler/qsc_circuit/src/builder.rs +++ b/source/compiler/qsc_circuit/src/builder.rs @@ -5,13 +5,15 @@ mod tests; use crate::{ + ComponentColumn, circuit::{ - Circuit, Ket, Measurement, Operation, PackageOffset, Qubit, Register, + Circuit, Ket, Measurement, Metadata, Operation, PackageOffset, Qubit, Register, ResolvedSourceLocation, SourceLocation, Unitary, operation_list_to_grid, }, operations::QubitParam, }; use qsc_data_structures::{ + functors::FunctorApp, index_map::IndexMap, line_column::{Encoding, Position}, }; @@ -20,10 +22,17 @@ use qsc_eval::{ debug::Frame, val::{self, Value}, }; -use qsc_fir::fir::PackageId; +use qsc_fir::fir::{self, PackageId, StoreItemId}; use qsc_frontend::compile::PackageStore; -use qsc_lowerer::map_fir_package_to_hir; -use std::{fmt::Write, mem::replace, rc::Rc}; +use qsc_hir::hir; +use qsc_lowerer::{map_fir_local_item_to_hir, map_fir_package_to_hir}; +use rustc_hash::FxHashSet; +use std::{ + fmt::{Debug, Write}, + hash::Hash, + mem::replace, + rc::Rc, +}; /// Circuit builder that implements the `Tracer` trait to build a circuit /// while tracing execution. @@ -58,7 +67,7 @@ impl Tracer for CircuitTracer { controls: &[usize], theta: Option, ) { - let called_at = self.user_code_call_location(stack); + let called_at = map_stack_frames_to_locations(stack); let display_args: Vec = theta.map(|p| format!("{p:.4}")).into_iter().collect(); self.circuit_builder.gate( self.wire_map_builder.current(), @@ -71,7 +80,7 @@ impl Tracer for CircuitTracer { } fn measure(&mut self, stack: &[Frame], name: &str, q: usize, val: &val::Result) { - let called_at = self.user_code_call_location(stack); + let called_at = map_stack_frames_to_locations(stack); let r = match val { val::Result::Id(id) => *id, val::Result::Loss | val::Result::Val(_) => { @@ -91,7 +100,7 @@ impl Tracer for CircuitTracer { } fn reset(&mut self, stack: &[Frame], q: usize) { - let called_at = self.user_code_call_location(stack); + let called_at = map_stack_frames_to_locations(stack); self.circuit_builder .reset(self.wire_map_builder.current(), q, called_at); } @@ -120,7 +129,7 @@ impl Tracer for CircuitTracer { } else { vec![classical_args] }, - self.user_code_call_location(stack), + map_stack_frames_to_locations(stack), ); } @@ -135,7 +144,12 @@ impl CircuitTracer { CircuitTracer { config, wire_map_builder: WireMapBuilder::new(vec![]), - circuit_builder: OperationListBuilder::new(config.max_operations), + circuit_builder: OperationListBuilder::new( + config.max_operations, + user_package_ids.to_vec(), + config.group_by_scope, + config.source_locations, + ), next_result_id: 0, user_package_ids: user_package_ids.to_vec(), } @@ -169,7 +183,12 @@ impl CircuitTracer { CircuitTracer { config, wire_map_builder: WireMapBuilder::new(params), - circuit_builder: OperationListBuilder::new(config.max_operations), + circuit_builder: OperationListBuilder::new( + config.max_operations, + user_package_ids.to_vec(), + config.group_by_scope, + config.source_locations, + ), next_result_id: 0, user_package_ids: user_package_ids.to_vec(), } @@ -177,25 +196,35 @@ impl CircuitTracer { #[must_use] pub fn snapshot(&self, source_lookup: &impl SourceLookup) -> Circuit { - self.finish_circuit(self.circuit_builder.operations().clone(), source_lookup) + self.finish_circuit(self.circuit_builder.operations(), source_lookup) } #[must_use] pub fn finish(mut self, source_lookup: &impl SourceLookup) -> Circuit { let ops = replace( &mut self.circuit_builder, - OperationListBuilder::new(self.config.max_operations), + OperationListBuilder::new( + self.config.max_operations, + self.user_package_ids.clone(), + self.config.group_by_scope, + self.config.source_locations, + ), ) .into_operations(); - self.finish_circuit(ops, source_lookup) + self.finish_circuit(&ops, source_lookup) } fn finish_circuit( &self, - operations: Vec, + operations: &[OperationOrGroup], source_lookup: &impl SourceLookup, ) -> Circuit { + let operations = operations + .iter() + .map(|o| o.clone().into_operation(source_lookup)) + .collect(); + finish_circuit( self.wire_map_builder.wire_map.to_qubits(), operations, @@ -322,6 +351,7 @@ fn finish_circuit( pub trait SourceLookup { fn resolve_location(&self, package_offset: &PackageOffset) -> ResolvedSourceLocation; + fn resolve_scope(&self, scope: ScopeId) -> LexicalScope; } impl SourceLookup for PackageStore { @@ -347,6 +377,45 @@ impl SourceLookup for PackageStore { column: pos.column, } } + + fn resolve_scope(&self, scope_id: ScopeId) -> LexicalScope { + let ScopeId(item_id, functor_app) = scope_id; + let package = self + .get(map_fir_package_to_hir(item_id.package)) + .expect("package id must exist in store"); + + let item = package + .package + .items + .get(map_fir_local_item_to_hir(item_id.item)) + .expect("item id must exist in package"); + + let hir::ItemKind::Callable(callable_decl) = &item.kind else { + panic!("only callables should be in the stack"); + }; + + // Use the proper spec declaration from the source code + // depending on which functor application was used. + let spec_decl = match (functor_app.adjoint, functor_app.controlled) { + (false, 0) => Some(&callable_decl.body), + (false, _) => callable_decl.ctl.as_ref(), + (true, 0) => callable_decl.adj.as_ref(), + (true, _) => callable_decl.ctl_adj.as_ref(), + }; + + let spec_decl = spec_decl.unwrap_or(&callable_decl.body); + let scope_start_offset = spec_decl.span.lo; + let scope_name = callable_decl.name.name.clone(); + + LexicalScope::Callable { + location: PackageOffset { + package_id: item_id.package, + offset: scope_start_offset, + }, + name: scope_name, + functor_app, + } + } } fn resolve_locations(operations: &mut [Operation], source_location_lookup: &impl SourceLookup) { @@ -356,7 +425,11 @@ fn resolve_locations(operations: &mut [Operation], source_location_lookup: &impl resolve_locations(&mut column.components, source_location_lookup); } - if let Some(source) = op.source_mut() { + if let Some(source) = op.source_location_mut() { + resolve_source_location_if_unresolved(source, source_location_lookup); + } + + if let Some(source) = op.scope_location_mut() { resolve_source_location_if_unresolved(source, source_location_lookup); } } @@ -375,6 +448,7 @@ fn resolve_source_location_if_unresolved( } } +#[allow(clippy::struct_excessive_bools)] #[derive(Clone, Debug, Copy)] pub struct TracerConfig { /// Maximum number of operations the builder will add to the circuit @@ -382,6 +456,8 @@ pub struct TracerConfig { /// Capture the source code locations of operations and qubit declarations /// in the circuit diagram pub source_locations: bool, + /// Group operations according to call graph in the circuit diagram + pub group_by_scope: bool, } impl TracerConfig { @@ -399,6 +475,7 @@ impl Default for TracerConfig { Self { max_operations: Self::DEFAULT_MAX_OPERATIONS, source_locations: true, + group_by_scope: true, } } } @@ -448,11 +525,11 @@ impl WireMap { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ResultWire(pub usize, pub usize); +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct ResultWire(usize, usize); -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct QubitWire(pub usize); +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct QubitWire(usize); impl From for QubitWire { fn from(value: usize) -> Self { @@ -538,6 +615,304 @@ impl WireMapBuilder { } } +#[derive(Clone, Debug)] +struct OperationOrGroup { + kind: OperationOrGroupKind, + op: Operation, +} + +fn map_stack_frames_to_locations(stack: &[Frame]) -> Vec { + stack + .iter() + .map(|frame| { + LocationMetadata::new( + PackageOffset { + package_id: frame.id.package, + offset: frame.span.lo, + }, + ScopeId(frame.id, frame.functor), + ) + }) + .collect::>() +} + +#[derive(Clone, Debug)] +enum OperationOrGroupKind { + Single, + Group { + scope_stack: ScopeStack, + children: Vec, + }, +} + +impl OperationOrGroup { + fn new_single(op: Operation) -> Self { + Self { + kind: OperationOrGroupKind::Single, + op, + } + } + + fn new_unitary( + name: &str, + is_adjoint: bool, + targets: &[QubitWire], + controls: &[QubitWire], + args: Vec, + ) -> Self { + Self::new_single(Operation::Unitary(Unitary { + gate: name.to_string(), + args, + children: vec![], + targets: targets + .iter() + .map(|q| Register { + qubit: q.0, + result: None, + }) + .collect(), + controls: controls + .iter() + .map(|q| Register { + qubit: q.0, + result: None, + }) + .collect(), + is_adjoint, + metadata: Some(Metadata { + source: None, + scope_location: None, + }), + })) + } + + fn new_measurement(label: &str, qubit: QubitWire, result: ResultWire) -> Self { + Self::new_single(Operation::Measurement(Measurement { + gate: label.to_string(), + args: vec![], + children: vec![], + qubits: vec![Register { + qubit: qubit.0, + result: None, + }], + results: vec![Register { + qubit: result.0, + result: Some(result.1), + }], + metadata: None, + })) + } + + fn new_ket(qubit: QubitWire) -> Self { + Self::new_single(Operation::Ket(Ket { + gate: "0".to_string(), + args: vec![], + children: vec![], + targets: vec![Register { + qubit: qubit.0, + result: None, + }], + metadata: None, + })) + } + + fn all_qubits(&self) -> Vec { + let qubits: FxHashSet = match &self.op { + Operation::Measurement(measurement) => measurement.qubits.clone(), + Operation::Unitary(unitary) => unitary + .targets + .iter() + .chain(unitary.controls.iter()) + .filter(|r| r.result.is_none()) + .cloned() + .collect(), + Operation::Ket(ket) => ket.targets.clone(), + } + .into_iter() + .map(|r| QubitWire(r.qubit)) + .collect(); + qubits.into_iter().collect() + } + + fn all_results(&self) -> Vec { + let results: FxHashSet = match &self.op { + Operation::Measurement(measurement) => measurement + .results + .iter() + .filter_map(|r| r.result.map(|res| ResultWire(r.qubit, res))) + .collect(), + Operation::Unitary(unitary) => unitary + .targets + .iter() + .chain(unitary.controls.iter()) + .filter_map(|r| r.result.map(|res| ResultWire(r.qubit, res))) + .collect(), + Operation::Ket(_) => vec![], + } + .into_iter() + .collect(); + results.into_iter().collect() + } + + fn children_mut(&mut self) -> Option<&mut Vec> + where + Self: std::marker::Sized, + { + if let OperationOrGroupKind::Group { children, .. } = &mut self.kind { + Some(children) + } else { + None + } + } + + fn new_group(scope_stack: ScopeStack, children: Vec) -> Self { + let all_qubits = children + .iter() + .flat_map(OperationOrGroup::all_qubits) + .collect::>() + .into_iter() + .collect::>(); + + let all_results = children + .iter() + .flat_map(OperationOrGroup::all_results) + .collect::>() + .into_iter() + .collect::>(); + + Self { + kind: OperationOrGroupKind::Group { + scope_stack, + children, + }, + op: Operation::Unitary(Unitary { + gate: String::new(), // to be filled in later + args: vec![], + children: vec![], + targets: all_qubits + .iter() + .map(|q| Register { + qubit: q.0, + result: None, + }) + .chain(all_results.iter().map(|r| Register { + qubit: r.0, + result: Some(r.1), + })) + .collect(), + controls: vec![], + is_adjoint: false, + metadata: Some(Metadata { + source: None, + scope_location: None, + }), + }), + } + } + + fn extend_target_qubits(&mut self, target_qubits: &[QubitWire]) { + match &mut self.op { + Operation::Measurement(_) => {} + Operation::Unitary(unitary) => { + unitary + .targets + .extend(target_qubits.iter().map(|q| Register { + qubit: q.0, + result: None, + })); + unitary + .targets + .sort_unstable_by_key(|r| (r.qubit, r.result)); + unitary.targets.dedup(); + } + Operation::Ket(ket) => { + ket.targets.extend(target_qubits.iter().map(|q| Register { + qubit: q.0, + result: None, + })); + } + } + } + + fn extend_target_results(&mut self, target_results: &[ResultWire]) { + match &mut self.op { + Operation::Measurement(measurement) => { + measurement + .results + .extend(target_results.iter().map(|r| Register { + qubit: r.0, + result: Some(r.1), + })); + measurement + .results + .sort_unstable_by_key(|reg| (reg.qubit, reg.result)); + measurement.results.dedup(); + } + Operation::Unitary(unitary) => { + unitary + .targets + .extend(target_results.iter().map(|r| Register { + qubit: r.0, + result: Some(r.1), + })); + unitary + .targets + .sort_unstable_by_key(|r| (r.qubit, r.result)); + unitary.targets.dedup(); + } + Operation::Ket(_) => {} + } + } + + fn scope_stack_if_group(&self) -> Option<&ScopeStack> { + if let OperationOrGroupKind::Group { scope_stack, .. } = &self.kind { + Some(scope_stack) + } else { + None + } + } + + fn set_location(&mut self, location: PackageOffset) { + self.op + .source_location_mut() + .replace(SourceLocation::Unresolved(location)); + } + + fn into_operation(mut self, scope_resolver: &impl SourceLookup) -> Operation { + match self.kind { + OperationOrGroupKind::Single => self.op, + OperationOrGroupKind::Group { + scope_stack, + children, + } => { + let Operation::Unitary(u) = &mut self.op else { + panic!("group operation should be a unitary") + }; + let scope = scope_stack.resolve_scope(scope_resolver); + let scope_location = scope.location(); + u.gate = scope.name(); + u.is_adjoint = scope.is_adjoint(); + let scope_location = scope_location.map(SourceLocation::Unresolved); + if let Some(metadata) = &mut u.metadata { + metadata.scope_location = scope_location; + } else { + u.metadata = Some(Metadata { + source: None, + scope_location, + }); + } + u.children = vec![ComponentColumn { + components: children + .into_iter() + .map(|o| o.into_operation(scope_resolver)) + .collect(), + }]; + self.op + } + } + } +} + /// Builds a list of circuit operations with a maximum operation limit. /// Stops adding operations once the limit is exceeded. /// @@ -546,33 +921,65 @@ impl WireMapBuilder { struct OperationListBuilder { max_ops: usize, max_ops_exceeded: bool, - operations: Vec, + operations: Vec, + source_locations: bool, + user_package_ids: Vec, + group_by_scope: bool, } impl OperationListBuilder { - fn new(max_operations: usize) -> Self { + fn new( + max_operations: usize, + user_package_ids: Vec, + group_by_scope: bool, + source_locations: bool, + ) -> Self { Self { max_ops: max_operations, max_ops_exceeded: false, operations: vec![], + source_locations, + user_package_ids, + group_by_scope, } } - fn push_op(&mut self, op: Operation) { + fn push_op(&mut self, mut op: OperationOrGroup, unfiltered_call_stack: Vec) { if self.max_ops_exceeded || self.operations.len() >= self.max_ops { // Stop adding gates and leave the circuit as is self.max_ops_exceeded = true; return; } - self.operations.push(op); + let op_call_stack = if self.group_by_scope || self.source_locations { + retain_user_frames(&self.user_package_ids, unfiltered_call_stack) + } else { + vec![] + }; + + if self.source_locations + && let Some(called_at) = op_call_stack.last() + { + op.set_location(called_at.source_location()); + } + + add_scoped_op( + &mut self.operations, + &ScopeStack::top(), + op, + if self.group_by_scope { + &op_call_stack + } else { + &[] + }, + ); } - fn operations(&self) -> &Vec { + fn operations(&self) -> &Vec { &self.operations } - fn into_operations(self) -> Vec { + fn into_operations(self) -> Vec { self.operations } @@ -583,31 +990,22 @@ impl OperationListBuilder { is_adjoint: bool, inputs: &GateInputs, args: Vec, - called_at: Option, + call_stack: Vec, ) { - self.push_op(Operation::Unitary(Unitary { - gate: name.to_string(), - args, - children: vec![], - targets: inputs - .targets - .iter() - .map(|q| Register { - qubit: wire_map.qubit_wire(*q).0, - result: None, - }) - .collect(), - controls: inputs - .controls - .iter() - .map(|q| Register { - qubit: wire_map.qubit_wire(*q).0, - result: None, - }) - .collect(), - is_adjoint, - source: called_at.map(SourceLocation::Unresolved), - })); + let targets = inputs + .targets + .iter() + .map(|q| wire_map.qubit_wire(*q)) + .collect::>(); + let controls = inputs + .controls + .iter() + .map(|q| wire_map.qubit_wire(*q)) + .collect::>(); + self.push_op( + OperationOrGroup::new_unitary(name, is_adjoint, &targets, &controls, args), + call_stack, + ); } fn m( @@ -615,24 +1013,14 @@ impl OperationListBuilder { wire_map: &WireMap, qubit: usize, result: usize, - called_at: Option, + call_stack: Vec, ) { - let result_wire = wire_map.result_wire(result); - - self.push_op(Operation::Measurement(Measurement { - gate: "M".to_string(), - args: vec![], - children: vec![], - qubits: vec![Register { - qubit: wire_map.qubit_wire(qubit).0, - result: None, - }], - results: vec![Register { - qubit: result_wire.0, - result: Some(result_wire.1), - }], - source: called_at.map(SourceLocation::Unresolved), - })); + let qubit = wire_map.qubit_wire(qubit); + let result = wire_map.result_wire(result); + self.push_op( + OperationOrGroup::new_measurement("M", qubit, result), + call_stack, + ); } fn mresetz( @@ -640,48 +1028,20 @@ impl OperationListBuilder { wire_map: &WireMap, qubit: usize, result: usize, - called_at: Option, + call_stack: Vec, ) { - let qubits: Vec = vec![Register { - qubit: wire_map.qubit_wire(qubit).0, - result: None, - }]; - - let resul_wire = wire_map.result_wire(result); - let results = vec![Register { - qubit: resul_wire.0, - result: Some(resul_wire.1), - }]; - - self.push_op(Operation::Measurement(Measurement { - gate: "MResetZ".to_string(), - args: vec![], - children: vec![], - qubits: qubits.clone(), - results, - source: called_at.map(SourceLocation::Unresolved), - })); - - self.push_op(Operation::Ket(Ket { - gate: "0".to_string(), - args: vec![], - children: vec![], - targets: qubits, - source: called_at.map(SourceLocation::Unresolved), - })); + let qubit = wire_map.qubit_wire(qubit); + let result = wire_map.result_wire(result); + self.push_op( + OperationOrGroup::new_measurement("MResetZ", qubit, result), + call_stack.clone(), + ); + self.push_op(OperationOrGroup::new_ket(qubit), call_stack); } - fn reset(&mut self, wire_map: &WireMap, qubit: usize, called_at: Option) { - self.push_op(Operation::Ket(Ket { - gate: "0".to_string(), - args: vec![], - children: vec![], - targets: vec![Register { - qubit: wire_map.qubit_wire(qubit).0, - result: None, - }], - source: called_at.map(SourceLocation::Unresolved), - })); + fn reset(&mut self, wire_map: &WireMap, qubit: usize, call_stack: Vec) { + let qubit = wire_map.qubit_wire(qubit); + self.push_op(OperationOrGroup::new_ket(qubit), call_stack); } } @@ -689,3 +1049,241 @@ struct GateInputs<'a> { targets: &'a [usize], controls: &'a [usize], } + +#[derive(Clone, Debug, PartialEq, Eq, Copy)] +pub struct ScopeId(StoreItemId, FunctorApp); + +impl Default for ScopeId { + /// Default represents the "Top" scope + fn default() -> Self { + ScopeId( + StoreItemId { + package: usize::MAX.into(), + item: usize::MAX.into(), + }, + FunctorApp { + adjoint: false, + controlled: 0, + }, + ) + } +} + +impl Hash for ScopeId { + fn hash(&self, state: &mut H) { + self.0.hash(state); + let FunctorApp { + adjoint, + controlled, + } = self.1; + adjoint.hash(state); + controlled.hash(state); + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LexicalScope { + Top, + Callable { + name: Rc, + functor_app: FunctorApp, + location: PackageOffset, + }, +} + +impl LexicalScope { + fn top() -> Self { + LexicalScope::Top + } + + fn location(&self) -> Option { + match self { + LexicalScope::Top => None, + LexicalScope::Callable { location, .. } => Some(*location), + } + } + + fn name(&self) -> String { + match self { + LexicalScope::Top => "Top".to_string(), + LexicalScope::Callable { name, .. } => name.to_string(), + } + } + + fn is_adjoint(&self) -> bool { + match self { + LexicalScope::Top => false, + LexicalScope::Callable { functor_app, .. } => functor_app.adjoint, + } + } +} + +/// Inserts an operation into a hierarchical structure that mirrors the call stack. +/// +/// This function collapses flat call stack traces into nested groups, creating a tree structure +/// where operations are organized by the lexical scopes (functions/operations) they were called from. +/// +/// In principle, this is similar to how a profiling tool, such as flamegraph's stackCollapse, +/// would collapse a series of call stacks into a call hierarchy. +/// +/// This allows circuit visualizations to show operations grouped by their calling context. +fn add_scoped_op( + current_container: &mut Vec, + current_scope_stack: &ScopeStack, + op: OperationOrGroup, + op_call_stack: &[LocationMetadata], +) { + let relative_stack = strip_scope_stack_prefix( + op_call_stack, + current_scope_stack, + ).expect("op_call_stack_rel should be a suffix of op_call_stack_abs after removing current_scope_stack_abs"); + + if !relative_stack.is_empty() { + if let Some(last_op) = current_container.last_mut() { + // See if we can add to the last scope inside the current container + if let Some(last_scope_stack) = last_op.scope_stack_if_group() + && strip_scope_stack_prefix(op_call_stack, last_scope_stack).is_some() + { + // The last scope matched, add to it + let last_scope_stack = last_scope_stack.clone(); + + last_op.extend_target_qubits(&op.all_qubits()); + last_op.extend_target_results(&op.all_results()); + let last_op_children = last_op.children_mut().expect("operation should be a group"); + + // Recursively add to the children + add_scoped_op(last_op_children, &last_scope_stack, op, op_call_stack); + + return; + } + } + + let op_scope_stack = scope_stack(op_call_stack); + if *current_scope_stack != op_scope_stack { + // Need to create a new scope group + let scope_group = OperationOrGroup::new_group(op_scope_stack, vec![op]); + + let parent = op_call_stack + .split_last() + .expect("should have more than one etc") + .1 + .to_vec(); + // Recursively add the new scope group to the current container + add_scoped_op(current_container, current_scope_stack, scope_group, &parent); + + return; + } + } + + current_container.push(op); +} + +fn retain_user_frames( + user_package_ids: &[PackageId], + mut location_stack: Vec, +) -> Vec { + location_stack.retain(|location| { + let package_id = location.package_id(); + user_package_ids.is_empty() || user_package_ids.contains(&package_id) + }); + + location_stack +} + +/// Represents a location in the source code along with its lexical scope. +#[derive(Clone, Debug, PartialEq, Eq)] +struct LocationMetadata { + location: PackageOffset, + scope_id: ScopeId, +} + +impl LocationMetadata { + fn new(location: PackageOffset, scope_id: ScopeId) -> Self { + Self { location, scope_id } + } + fn lexical_scope(&self) -> ScopeId { + self.scope_id + } + + fn package_id(&self) -> fir::PackageId { + self.location.package_id + } + + fn source_location(&self) -> PackageOffset { + self.location + } +} + +/// Represents a scope in the call stack, tracking the caller chain and current scope identifier. +#[derive(Clone, Debug, PartialEq)] +struct ScopeStack { + caller: Vec, + scope: ScopeId, +} + +impl ScopeStack { + fn caller(&self) -> &[LocationMetadata] { + &self.caller + } + + fn current_lexical_scope(&self) -> ScopeId { + assert!(!self.is_top(), "top scope has no lexical scope"); + self.scope + } + + fn is_top(&self) -> bool { + self.caller.is_empty() && self.scope == ScopeId::default() + } + + fn top() -> Self { + ScopeStack { + caller: Vec::new(), + scope: ScopeId::default(), + } + } + + fn resolve_scope(&self, scope_resolver: &impl SourceLookup) -> LexicalScope { + if self.is_top() { + LexicalScope::top() + } else { + scope_resolver.resolve_scope(self.scope) + } + } +} + +/// Strips a scope stack prefix from a call stack. +/// +/// The `full_call_stack` parameter represents a complete call stack, while +/// `prefix_scope_stack` represents a scope stack to match against. +/// +/// If `prefix_scope_stack` is not a prefix of `full_call_stack`, this function returns `None`. +/// +/// If it is a prefix, this function returns the remainder of `full_call_stack` after removing +/// the prefix, starting from the first location in the call stack that is in the scope of +/// `prefix_scope_stack.scope`. +fn strip_scope_stack_prefix( + full_call_stack: &[LocationMetadata], + prefix_scope_stack: &ScopeStack, +) -> Option> { + if prefix_scope_stack.is_top() { + return Some(full_call_stack.to_vec()); + } + + if full_call_stack.len() > prefix_scope_stack.caller().len() + && let Some(rest) = full_call_stack.strip_prefix(prefix_scope_stack.caller()) + && rest[0].lexical_scope() == prefix_scope_stack.current_lexical_scope() + { + assert!(!rest.is_empty()); + return Some(rest.to_vec()); + } + None +} + +fn scope_stack(instruction_stack: &[LocationMetadata]) -> ScopeStack { + instruction_stack + .split_last() + .map_or(ScopeStack::top(), |(youngest, prefix)| ScopeStack { + caller: prefix.to_vec(), + scope: youngest.lexical_scope(), + }) +} diff --git a/source/compiler/qsc_circuit/src/builder/tests.rs b/source/compiler/qsc_circuit/src/builder/tests.rs index a675453e5b..8518d1f973 100644 --- a/source/compiler/qsc_circuit/src/builder/tests.rs +++ b/source/compiler/qsc_circuit/src/builder/tests.rs @@ -1,11 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +mod group_scopes; + +use std::vec; + use super::*; use expect_test::expect; -use qsc_data_structures::span::Span; +use qsc_data_structures::{functors::FunctorApp, span::Span}; +use rustc_hash::FxHashMap; -struct FakeCompilation {} +#[derive(Default)] +struct FakeCompilation { + scopes: Scopes, +} impl SourceLookup for FakeCompilation { fn resolve_location(&self, package_offset: &PackageOffset) -> ResolvedSourceLocation { @@ -19,7 +27,25 @@ impl SourceLookup for FakeCompilation { column: package_offset.offset, } } + + fn resolve_scope(&self, scope_id: ScopeId) -> LexicalScope { + let name = self + .scopes + .id_to_name + .get(&scope_id.0) + .expect("unknown scope id") + .clone(); + LexicalScope::Callable { + name, + location: PackageOffset { + package_id: scope_id.0.package, + offset: 0, + }, + functor_app: scope_id.1, + } + } } + impl FakeCompilation { const LIBRARY_PACKAGE_ID: usize = 0; const USER_PACKAGE_ID: usize = 2; @@ -28,36 +54,80 @@ impl FakeCompilation { vec![Self::USER_PACKAGE_ID.into()] } - fn library_frame(offset: u32) -> Frame { - Self::frame(Self::LIBRARY_PACKAGE_ID, offset) + fn library_frame(&mut self, offset: u32) -> Frame { + let scope_id = + self.scopes + .get_or_create_scope(Self::LIBRARY_PACKAGE_ID, "library_item", false); + Self::frame(scope_id, offset, false) + } + + fn user_code_frame(&mut self, scope_name: &str, offset: u32) -> Frame { + let scope_id = self + .scopes + .get_or_create_scope(Self::USER_PACKAGE_ID, scope_name, false); + Self::frame(scope_id, offset, false) } - fn user_code_frame(offset: u32) -> Frame { - Self::frame(Self::USER_PACKAGE_ID, offset) + fn user_code_adjoint_frame(&mut self, scope_name: &str, offset: u32) -> Frame { + let scope_id = self + .scopes + .get_or_create_scope(Self::USER_PACKAGE_ID, scope_name, true); + Self::frame(scope_id, offset, true) } - fn frame(package_id: usize, offset: u32) -> Frame { + fn frame(scope_item_id: ScopeId, offset: u32, is_adjoint: bool) -> Frame { Frame { span: Span { lo: offset, hi: offset + 1, }, - id: qsc_fir::fir::StoreItemId { - package: package_id.into(), - item: 0.into(), + id: scope_item_id.0, + caller: PackageId::CORE, // unused in tests + functor: FunctorApp { + adjoint: is_adjoint, + controlled: 0, }, - caller: PackageId::CORE, - functor: Default::default(), } } } +#[derive(Default)] +struct Scopes { + id_to_name: FxHashMap>, + name_to_id: FxHashMap, StoreItemId>, +} + +impl Scopes { + fn get_or_create_scope(&mut self, package_id: usize, name: &str, is_adjoint: bool) -> ScopeId { + let name: Rc = name.into(); + let item_id = if let Some(item_id) = self.name_to_id.get(&name) { + *item_id + } else { + let item_id = StoreItemId { + package: package_id.into(), + item: self.id_to_name.len().into(), + }; + self.id_to_name.insert(item_id, name.clone()); + self.name_to_id.insert(name, item_id); + item_id + }; + ScopeId( + item_id, + FunctorApp { + adjoint: is_adjoint, + controlled: 0, + }, + ) + } +} + #[test] fn exceed_max_operations() { let mut builder = CircuitTracer::new( TracerConfig { max_operations: 2, source_locations: false, + group_by_scope: false, }, &FakeCompilation::user_package_ids(), ); @@ -68,7 +138,7 @@ fn exceed_max_operations() { builder.gate(&[], "X", false, &[0], &[], None); builder.gate(&[], "X", false, &[0], &[], None); - let circuit = builder.finish(&FakeCompilation {}); + let circuit = builder.finish(&FakeCompilation::default()); // The current behavior is to silently truncate the circuit // if it exceeds the maximum allowed number of operations. @@ -80,10 +150,12 @@ fn exceed_max_operations() { #[test] fn source_locations_enabled() { + let mut c = FakeCompilation::default(); let mut builder = CircuitTracer::new( TracerConfig { max_operations: 10, source_locations: true, + group_by_scope: false, }, &FakeCompilation::user_package_ids(), ); @@ -91,7 +163,7 @@ fn source_locations_enabled() { builder.qubit_allocate(&[], 0); builder.gate( - &[FakeCompilation::user_code_frame(10)], + &[c.user_code_frame("Main", 10)], "X", false, &[0], @@ -99,7 +171,7 @@ fn source_locations_enabled() { None, ); - let circuit = builder.finish(&FakeCompilation {}); + let circuit = builder.finish(&c); expect![[r#" q_0 ─ X@user_code.qs:0:10 ─ @@ -115,10 +187,12 @@ fn source_locations_enabled() { #[test] fn source_locations_disabled() { + let mut c = FakeCompilation::default(); let mut builder = CircuitTracer::new( TracerConfig { max_operations: 10, source_locations: false, + group_by_scope: false, }, &FakeCompilation::user_package_ids(), ); @@ -126,7 +200,7 @@ fn source_locations_disabled() { builder.qubit_allocate(&[], 0); builder.gate( - &[FakeCompilation::user_code_frame(10)], + &[c.user_code_frame("Main", 10)], "X", false, &[0], @@ -134,7 +208,7 @@ fn source_locations_disabled() { None, ); - let circuit = builder.finish(&FakeCompilation {}); + let circuit = builder.finish(&c); expect![[r#" q_0 ── X ── @@ -144,10 +218,12 @@ fn source_locations_disabled() { #[test] fn source_locations_multiple_user_frames() { + let mut c = FakeCompilation::default(); let mut builder = CircuitTracer::new( TracerConfig { max_operations: 10, source_locations: true, + group_by_scope: false, }, &FakeCompilation::user_package_ids(), ); @@ -155,10 +231,7 @@ fn source_locations_multiple_user_frames() { builder.qubit_allocate(&[], 0); builder.gate( - &[ - FakeCompilation::user_code_frame(10), - FakeCompilation::user_code_frame(20), - ], + &[c.user_code_frame("Main", 10), c.user_code_frame("Main", 20)], "X", false, &[0], @@ -166,7 +239,7 @@ fn source_locations_multiple_user_frames() { None, ); - let circuit = builder.finish(&FakeCompilation {}); + let circuit = builder.finish(&c); // Use the most current user frame for the source location. expect![[r#" @@ -183,20 +256,19 @@ fn source_locations_multiple_user_frames() { #[test] fn source_locations_library_frames_excluded() { + let mut c = FakeCompilation::default(); let mut builder = CircuitTracer::new( TracerConfig { max_operations: 10, source_locations: true, + group_by_scope: false, }, &FakeCompilation::user_package_ids(), ); builder.qubit_allocate(&[], 0); builder.gate( - &[ - FakeCompilation::user_code_frame(10), - FakeCompilation::library_frame(20), - ], + &[c.user_code_frame("Main", 10), c.library_frame(20)], "X", false, &[0], @@ -204,7 +276,7 @@ fn source_locations_library_frames_excluded() { None, ); - let circuit = builder.finish(&FakeCompilation {}); + let circuit = builder.finish(&c); // Most recent frame is a library frame - source // location should fall back to the nearest user frame. @@ -216,10 +288,12 @@ fn source_locations_library_frames_excluded() { #[test] fn source_locations_only_library_frames() { + let mut c = FakeCompilation::default(); let mut builder = CircuitTracer::new( TracerConfig { max_operations: 10, source_locations: true, + group_by_scope: false, }, &FakeCompilation::user_package_ids(), ); @@ -227,10 +301,7 @@ fn source_locations_only_library_frames() { builder.qubit_allocate(&[], 0); builder.gate( - &[ - FakeCompilation::library_frame(20), - FakeCompilation::library_frame(30), - ], + &[c.library_frame(20), c.library_frame(30)], "X", false, &[0], @@ -238,7 +309,7 @@ fn source_locations_only_library_frames() { None, ); - let circuit = builder.finish(&FakeCompilation {}); + let circuit = builder.finish(&c); // Only library frames, no user source to show expect![[r#" @@ -249,10 +320,12 @@ fn source_locations_only_library_frames() { #[test] fn source_locations_enabled_no_stack() { + let c = FakeCompilation::default(); let mut builder = CircuitTracer::new( TracerConfig { max_operations: 10, source_locations: true, + group_by_scope: false, }, &FakeCompilation::user_package_ids(), ); @@ -261,7 +334,7 @@ fn source_locations_enabled_no_stack() { builder.gate(&[], "X", false, &[0], &[], None); - let circuit = builder.finish(&FakeCompilation {}); + let circuit = builder.finish(&c); // No stack was passed, so no source location to show expect![[r#" @@ -272,32 +345,36 @@ fn source_locations_enabled_no_stack() { #[test] fn qubit_source_locations_via_stack() { + let mut c = FakeCompilation::default(); let mut builder = CircuitTracer::new( TracerConfig { max_operations: 10, source_locations: true, + group_by_scope: false, }, &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[FakeCompilation::user_code_frame(10)], 0); + builder.qubit_allocate(&[c.user_code_frame("Main", 10)], 0); builder.gate(&[], "X", false, &[0], &[], None); - let circuit = builder.finish(&FakeCompilation {}); + let circuit = builder.finish(&c); expect![[r#" - q_0@user_code.qs:0:10 ── X ── + q_0@user_code.qs:0:10 ── X ── "#]] .assert_eq(&circuit.to_string()); } #[test] fn qubit_labels_for_preallocated_qubits() { + let mut c = FakeCompilation::default(); let mut builder = CircuitTracer::with_qubit_input_params( TracerConfig { max_operations: 10, source_locations: true, + group_by_scope: false, }, &FakeCompilation::user_package_ids(), Some(( @@ -312,7 +389,7 @@ fn qubit_labels_for_preallocated_qubits() { builder.qubit_allocate(&[], 0); builder.gate( - &[FakeCompilation::user_code_frame(20)], + &[c.user_code_frame("Main", 20)], "X", false, &[0], @@ -320,11 +397,11 @@ fn qubit_labels_for_preallocated_qubits() { None, ); - let circuit = builder.finish(&FakeCompilation {}); + let circuit = builder.finish(&c); expect![[r#" - q_0@user_code.qs:0:10 ─ X@user_code.qs:0:20 ─ - q_1@user_code.qs:0:10 ─────────────────────── + q_0@user_code.qs:0:10 ─ X@user_code.qs:0:20 ─ + q_1@user_code.qs:0:10 ─────────────────────── "#]] .assert_eq(&circuit.to_string()); } diff --git a/source/compiler/qsc_circuit/src/builder/tests/group_scopes.rs b/source/compiler/qsc_circuit/src/builder/tests/group_scopes.rs new file mode 100644 index 0000000000..675e1c165c --- /dev/null +++ b/source/compiler/qsc_circuit/src/builder/tests/group_scopes.rs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::FakeCompilation; +use crate::{CircuitTracer, TracerConfig}; +use expect_test::{Expect, expect}; +use qsc_eval::{backend::Tracer, debug::Frame}; + +fn check_groups(c: &FakeCompilation, instructions: &[(Vec, &str)], expect: &Expect) { + let mut tracer = CircuitTracer::new( + TracerConfig { + max_operations: usize::MAX, + source_locations: false, + group_by_scope: true, + }, + &FakeCompilation::user_package_ids(), + ); + + let qubit_id = 0; + + // Allocate qubit 0 + tracer.qubit_allocate(&[], qubit_id); + + // Trace each instruction, applying it to qubit 0 + for i in instructions { + let stack = &i.0; + let name = i.1; + + tracer.gate(stack, name, false, &[qubit_id], &[], None); + } + + let circuit = tracer.finish(c); + expect.assert_eq(&circuit.display_with_groups().to_string()); +} + +#[test] +fn empty() { + check_groups( + &FakeCompilation::default(), + &[], + &expect![[r#" + q_0 + "#]], + ); +} + +#[test] +fn single_op() { + let mut c = FakeCompilation::default(); + let program = vec![(vec![c.user_code_frame("Main", 1)], "H")]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [Main] ──── H ──── ] ── + "#]], + ); +} + +#[test] +fn two_ops_in_same_scope() { + let mut c = FakeCompilation::default(); + let program = vec![ + (vec![c.user_code_frame("Main", 1)], "H"), + (vec![c.user_code_frame("Main", 2)], "X"), + ]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [Main] ──── H ──── X ──── ] ── + "#]], + ); +} + +#[test] +fn two_ops_in_separate_scopes() { + let mut c = FakeCompilation::default(); + let program = vec![ + (vec![c.user_code_frame("Foo", 1)], "H"), + (vec![c.user_code_frame("Bar", 2)], "X"), + ]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [Foo] ─── H ──── ] ─── [ [Bar] ─── X ──── ] ── + "#]], + ); +} + +#[test] +fn two_ops_same_grandparent() { + let mut c = FakeCompilation::default(); + let program = vec![ + ( + vec![c.user_code_frame("Main", 1), c.user_code_frame("Foo", 2)], + "H", + ), + ( + vec![c.user_code_frame("Main", 1), c.user_code_frame("Bar", 3)], + "X", + ), + ]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [Main] ─── [ [Foo] ─── H ──── ] ─── [ [Bar] ─── X ──── ] ──── ] ── + "#]], + ); +} + +#[test] +fn two_ops_same_parent_scope() { + let mut c = FakeCompilation::default(); + let program = vec![ + ( + vec![c.user_code_frame("Main", 1), c.user_code_frame("Foo", 2)], + "H", + ), + ( + vec![c.user_code_frame("Main", 1), c.user_code_frame("Foo", 3)], + "X", + ), + ]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [Main] ─── [ [Foo] ─── H ──── X ──── ] ──── ] ── + "#]], + ); +} + +#[test] +fn two_ops_separate_grandparents() { + let mut c = FakeCompilation::default(); + let program = vec![ + ( + vec![ + c.user_code_frame("A", 1), + c.user_code_frame("B", 3), + c.user_code_frame("C", 4), + ], + "X", + ), + ( + vec![ + c.user_code_frame("A", 2), + c.user_code_frame("B", 3), + c.user_code_frame("C", 4), + ], + "X", + ), + ]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [A] ── [ [B] ── [ [C] ─── X ──── ] ──── ] ─── [ [B] ── [ [C] ─── X ──── ] ──── ] ──── ] ── + "#]], + ); +} + +#[test] +fn same_grandparent_separate_parents() { + let mut c = FakeCompilation::default(); + let program = vec![ + ( + vec![ + c.user_code_frame("A", 2), + c.user_code_frame("B", 5), + c.user_code_frame("F", 11), + ], + "Z", + ), + ( + vec![ + c.user_code_frame("A", 2), + c.user_code_frame("B", 6), + c.user_code_frame("F", 10), + ], + "Y", + ), + ]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [A] ── [ [B] ── [ [F] ─── Z ──── ] ─── [ [F] ─── Y ──── ] ──── ] ──── ] ── + "#]], + ); +} + +#[test] +fn back_up_to_grandparent() { + let mut c = FakeCompilation::default(); + let program = vec![ + ( + vec![ + c.user_code_frame("A", 2), + c.user_code_frame("B", 6), + c.user_code_frame("C", 11), + ], + "X", + ), + (vec![c.user_code_frame("A", 1)], "Y"), + ]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [A] ── [ [B] ── [ [C] ─── X ──── ] ──── ] ──── Y ──── ] ── + "#]], + ); +} + +#[test] +fn library_frames_excluded() { + let mut c = FakeCompilation::default(); + let program = vec![ + ( + vec![ + c.library_frame(1), + c.user_code_frame("A", 2), + c.library_frame(2), + c.user_code_frame("B", 6), + c.library_frame(3), + ], + "X", + ), + (vec![c.library_frame(4)], "Y"), + ]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [A] ── [ [B] ─── X ──── ] ──── ] ──── Y ── + "#]], + ); +} + +#[test] +fn adjoint_call_frame() { + let mut c = FakeCompilation::default(); + let program = vec![ + ( + vec![ + c.user_code_frame("Main", 1), + c.library_frame(5), + c.user_code_frame("Foo", 2), + ], + "U", + ), + ( + vec![ + c.user_code_frame("Main", 1), + c.library_frame(5), + c.user_code_adjoint_frame("Foo", 3), + ], + "U", + ), + ]; + check_groups( + &c, + &program, + &expect![[r#" + q_0 ─ [ [Main] ─── [ [Foo] ─── U ──── ] ─── [ [Foo'] ──── U ──── ] ──── ] ── + "#]], + ); +} diff --git a/source/compiler/qsc_circuit/src/circuit.rs b/source/compiler/qsc_circuit/src/circuit.rs index f0aea70582..5c4832b62f 100644 --- a/source/compiler/qsc_circuit/src/circuit.rs +++ b/source/compiler/qsc_circuit/src/circuit.rs @@ -8,7 +8,7 @@ use qsc_fir::fir::PackageId; use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use std::{ - cmp::{self}, + cmp::max, fmt::{Display, Write}, hash::Hash, ops::Not, @@ -48,6 +48,18 @@ impl Circuit { CircuitDisplay { circuit: self, render_locations: false, + render_groups: false, + } + } + + #[must_use] + pub fn display_with_groups(&self) -> impl Display { + // Groups rendered only in tests since the current line rendering + // doesn't look good enough to be user-facing. + CircuitDisplay { + circuit: self, + render_locations: true, + render_groups: true, } } } @@ -60,6 +72,7 @@ impl Display for Circuit { CircuitDisplay { circuit: self, render_locations: true, + render_groups: false, } ) } @@ -101,6 +114,14 @@ impl Operation { } } + pub fn gate_mut(&mut self) -> &mut String { + match self { + Self::Measurement(measurement) => &mut measurement.gate, + Self::Unitary(unitary) => &mut unitary.gate, + Self::Ket(ket) => &mut ket.gate, + } + } + /// Returns the arguments for the operation. #[must_use] pub fn args(&self) -> Vec { @@ -111,21 +132,65 @@ impl Operation { } } - #[must_use] - pub fn source(&self) -> &Option { + pub fn args_mut(&mut self) -> &mut Vec { match self { - Self::Measurement(measurement) => &measurement.source, - Self::Unitary(unitary) => &unitary.source, - Self::Ket(ket) => &ket.source, + Self::Measurement(measurement) => &mut measurement.args, + Self::Unitary(unitary) => &mut unitary.args, + Self::Ket(ket) => &mut ket.args, } } #[must_use] - pub fn source_mut(&mut self) -> &mut Option { + pub fn source_location(&self) -> Option<&SourceLocation> { match self { - Self::Measurement(measurement) => &mut measurement.source, - Self::Unitary(unitary) => &mut unitary.source, - Self::Ket(ket) => &mut ket.source, + Self::Measurement(measurement) => measurement.metadata.as_ref(), + Self::Unitary(unitary) => unitary.metadata.as_ref(), + Self::Ket(ket) => ket.metadata.as_ref(), + } + .and_then(|m| m.source.as_ref()) + } + + #[must_use] + pub fn source_location_mut(&mut self) -> &mut Option { + let md = match self { + Self::Measurement(measurement) => &mut measurement.metadata, + Self::Unitary(unitary) => &mut unitary.metadata, + Self::Ket(ket) => &mut ket.metadata, + }; + + if md.is_none() { + md.replace(Metadata { + source: None, + scope_location: None, + }); + } + + if let Some(md) = md { + &mut md.source + } else { + unreachable!() + } + } + + #[must_use] + pub fn scope_location_mut(&mut self) -> &mut Option { + let md = match self { + Self::Measurement(measurement) => &mut measurement.metadata, + Self::Unitary(unitary) => &mut unitary.metadata, + Self::Ket(ket) => &mut ket.metadata, + }; + + if md.is_none() { + md.replace(Metadata { + source: None, + scope_location: None, + }); + } + + if let Some(md) = md { + &mut md.scope_location + } else { + unreachable!() } } @@ -190,7 +255,7 @@ pub struct Measurement { pub qubits: Vec, pub results: Vec, #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, + pub metadata: Option, } /// Representation of a unitary operation. @@ -212,7 +277,7 @@ pub struct Unitary { #[serde(default)] pub is_adjoint: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, + pub metadata: Option, } /// Representation of a gate that will set the target to a specific state. @@ -227,7 +292,7 @@ pub struct Ket { pub children: ComponentGrid, pub targets: Vec, #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, + pub metadata: Option, } #[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Clone)] @@ -271,6 +336,18 @@ pub struct Qubit { pub declarations: Vec, } +#[derive(Clone, Serialize, Deserialize, Debug)] +/// The schema of `Metadata` may change and its contents +/// are never meant to be persisted in a .qsc file. +pub struct Metadata { + #[serde(skip_serializing_if = "Option::is_none")] + /// The location in the source code that this operation originated from. + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// Only populated if this operation represents a scope group. + pub scope_location: Option, +} + #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum SourceLocation { @@ -279,7 +356,7 @@ pub enum SourceLocation { Unresolved(PackageOffset), } -#[derive(Clone, Debug, Copy)] +#[derive(Clone, Debug, Copy, PartialEq, Eq)] pub struct PackageOffset { pub package_id: PackageId, pub offset: u32, @@ -312,6 +389,7 @@ enum Wire { Classical { start_column: Option }, } +#[derive(Debug)] enum CircuitObject { Blank, Wire, @@ -413,25 +491,24 @@ impl Row { let mut s = String::new(); match &self.wire { Wire::Qubit { label } => { - s.write_str(&fmt_qubit_label(label))?; + s.write_str(&columns[0].fmt_qubit_label(label))?; for (column_index, column) in columns.iter().enumerate().skip(1) { - let val = self.objects.get(&column_index); - let object = val.unwrap_or(&CircuitObject::Wire); + let obj = self.objects.get(&column_index); - s.write_str(&column.fmt_qubit_circuit_object(object))?; + s.write_str(&column.fmt_object_on_qubit_wire(obj))?; } } Wire::Classical { start_column } => { for (column_index, column) in columns.iter().enumerate() { - let val = self.objects.get(&column_index); - - let object = match (val, start_column) { - (Some(v), _) => v, - (None, Some(s)) if column_index > *s => &CircuitObject::Wire, - _ => &CircuitObject::Blank, - }; + let obj = self.objects.get(&column_index); - s.write_str(&column.fmt_classical_circuit_object(object))?; + if let Some(start) = *start_column + && column_index > start + { + s.write_str(&column.fmt_object_on_classical_wire(obj))?; + } else { + s.write_str(&column.fmt_object(obj))?; + } } } } @@ -453,25 +530,10 @@ const VERTICAL_DASHED: [char; 3] = [' ', '┆', ' ']; // " ┆ " const VERTICAL: [char; 3] = [' ', '│', ' ']; // " │ " const BLANK: [char; 3] = [' ', ' ', ' ']; // " " -/// "q_0 " -#[allow(clippy::doc_markdown)] -fn fmt_qubit_label(label: &str) -> String { - let rest = MIN_COLUMN_WIDTH - 1; - format!("{label: Self { - Self { - column_width: MIN_COLUMN_WIDTH, - } - } -} - impl Column { fn new(column_width: usize) -> Self { // Column widths should be odd numbers for this struct to work well @@ -481,6 +543,14 @@ impl Column { } } + /// "q_0 " + #[allow(clippy::doc_markdown)] + fn fmt_qubit_label(&self, label: &str) -> String { + let column_width = self.column_width; + let s = format!("{label: String { let column_width = self.column_width; @@ -493,6 +563,12 @@ impl Column { format!("{:═^column_width$}", format!(" {obj} ")) } + /// " A " + fn fmt_on_blank(&self, obj: &str) -> String { + let column_width = self.column_width; + format!("{: ^column_width$}", format!(" {obj} ")) + } + fn expand_template(&self, template: &[char; 3]) -> String { let half_width = self.column_width / 2; let left = template[0].to_string().repeat(half_width); @@ -501,39 +577,63 @@ impl Column { format!("{left}{}{right}", template[1]) } - fn fmt_classical_circuit_object(&self, circuit_object: &CircuitObject) -> String { + fn fmt_object_on_classical_wire(&self, circuit_object: Option<&CircuitObject>) -> String { + let circuit_object = circuit_object.unwrap_or(&CircuitObject::Wire); + if let CircuitObject::Object(label) = circuit_object { return self.fmt_on_classical_wire(label.as_str()); } let template = match circuit_object { - CircuitObject::Blank => BLANK, CircuitObject::Wire => CLASSICAL_WIRE, - CircuitObject::WireCross => CLASSICAL_WIRE_CROSS, + CircuitObject::WireCross | CircuitObject::Vertical => CLASSICAL_WIRE_CROSS, CircuitObject::WireStart => CLASSICAL_WIRE_START, CircuitObject::DashedCross => CLASSICAL_WIRE_DASHED_CROSS, - CircuitObject::Vertical => VERTICAL, - CircuitObject::VerticalDashed => VERTICAL_DASHED, - CircuitObject::Object(_) => unreachable!("This case is covered in the early return."), + o @ (CircuitObject::VerticalDashed | CircuitObject::Blank) => { + unreachable!("unexpected object on blank row: {o:?}") + } + CircuitObject::Object(_) => unreachable!("case should have been handled earlier"), }; self.expand_template(&template) } - fn fmt_qubit_circuit_object(&self, circuit_object: &CircuitObject) -> String { + fn fmt_object_on_qubit_wire(&self, circuit_object: Option<&CircuitObject>) -> String { + let circuit_object = circuit_object.unwrap_or(&CircuitObject::Wire); if let CircuitObject::Object(label) = circuit_object { return self.fmt_on_qubit_wire(label.as_str()); } let template = match circuit_object { - CircuitObject::WireStart // This should never happen - | CircuitObject::Blank => BLANK, CircuitObject::Wire => QUBIT_WIRE, - CircuitObject::WireCross => QUBIT_WIRE_CROSS, + CircuitObject::WireCross | CircuitObject::Vertical => QUBIT_WIRE_CROSS, CircuitObject::DashedCross => QUBIT_WIRE_DASHED_CROSS, + CircuitObject::WireStart + | CircuitObject::VerticalDashed + | CircuitObject::Blank + | CircuitObject::Object(_) => unreachable!(), + }; + + self.expand_template(&template) + } + + fn fmt_object(&self, circuit_object: Option<&CircuitObject>) -> String { + let circuit_object = circuit_object.unwrap_or(&CircuitObject::Blank); + if let CircuitObject::Object(label) = circuit_object { + return self.fmt_on_blank(label.as_str()); + } + + let template = match circuit_object { + CircuitObject::WireStart => CLASSICAL_WIRE_START, + CircuitObject::Blank => BLANK, CircuitObject::Vertical => VERTICAL, CircuitObject::VerticalDashed => VERTICAL_DASHED, - CircuitObject::Object(_) => unreachable!("This case is covered in the early return."), + o @ (CircuitObject::Wire | CircuitObject::WireCross | CircuitObject::DashedCross) => { + unreachable!("unexpected object on blank row: {o:?}") + } + CircuitObject::Object(_) => { + unreachable!("case should have been handled earlier") + } }; self.expand_template(&template) @@ -543,6 +643,7 @@ impl Column { struct CircuitDisplay<'a> { circuit: &'a Circuit, render_locations: bool, + render_groups: bool, } impl Display for CircuitDisplay<'_> { @@ -565,7 +666,7 @@ impl Display for CircuitDisplay<'_> { self.initialize_rows(&mut rows, &mut register_to_row, &qubits_with_gap_row_below); // Add operations to the diagram - self.add_operations_to_diagram(&mut rows, ®ister_to_row); + self.add_grid(1, &self.circuit.component_grid, &mut rows, ®ister_to_row); // Finalize the diagram by extending wires and formatting columns let columns = finalize_columns(&rows); @@ -583,25 +684,39 @@ impl CircuitDisplay<'_> { /// Identifies qubits that require gap rows for multi-qubit operations. fn identify_qubits_with_gap_rows(&self, qubits_with_gap_row_below: &mut FxHashSet) { for col in &self.circuit.component_grid { - for op in &col.components { - let targets = match op { - Operation::Measurement(m) => &m.qubits, - Operation::Unitary(u) => &u.targets, - Operation::Ket(k) => &k.targets, - }; - for target in targets { - let qubit = target.qubit; - - if qubits_with_gap_row_below.contains(&qubit) { - continue; - } + Self::add_qubits_with_gap_rows(&col.components, qubits_with_gap_row_below); + } + } + + fn add_qubits_with_gap_rows( + components: &Vec, + qubits_with_gap_row_below: &mut FxHashSet, + ) { + for op in components { + if !op.children().is_empty() { + for c in op.children() { + Self::add_qubits_with_gap_rows(&c.components, qubits_with_gap_row_below); + } + continue; + } - let next_qubit = qubit + 1; + let targets = match op { + Operation::Measurement(m) => &m.qubits, + Operation::Unitary(u) => &u.targets, + Operation::Ket(k) => &k.targets, + }; + for target in targets { + let qubit = target.qubit; - // Check if the next qubit is also in this operation. - if targets.iter().any(|t| t.qubit == next_qubit) { - qubits_with_gap_row_below.insert(qubit); - } + if qubits_with_gap_row_below.contains(&qubit) { + continue; + } + + let next_qubit = qubit + 1; + + // Check if the next qubit is also in this operation. + if targets.iter().any(|t| t.qubit == next_qubit) { + qubits_with_gap_row_below.insert(qubit); } } } @@ -643,7 +758,7 @@ impl CircuitDisplay<'_> { // the next qubit, we add an empty row to make room for the vertical connector. // We can just use a classical wire type for this row since the wire won't actually be rendered. let extra_rows = if qubits_with_gap_row_below.contains(&q.id) { - cmp::max(1, q.num_results) + max(1, q.num_results) } else { q.num_results }; @@ -662,32 +777,98 @@ impl CircuitDisplay<'_> { } /// Adds operations to the diagram. - fn add_operations_to_diagram( + fn add_grid( &self, + start_column: usize, + component_grid: &ComponentGrid, rows: &mut [Row], register_to_row: &FxHashMap<(usize, Option), usize>, - ) { - for (col_index, col) in self.circuit.component_grid.iter().enumerate() { - for op in &col.components { - let targets = get_row_indexes(op, register_to_row, true); - let controls = get_row_indexes(op, register_to_row, false); - - let mut all_rows = targets.clone(); - all_rows.extend(controls.iter()); - all_rows.sort_unstable(); - - // We'll need to know the entire range of rows for this operation so we can - // figure out the starting column and also so we can draw any - // vertical lines that cross wires. - let (begin, end) = all_rows.split_first().map_or((0, 0), |(first, tail)| { - (*first, tail.last().unwrap_or(first) + 1) - }); + ) -> usize { + let mut curr_column = start_column; + for column_operations in component_grid { + let offset = self.add_column(rows, register_to_row, curr_column, column_operations); + curr_column += offset; + } + curr_column - start_column + } - let column = col_index + 1; + fn add_column( + &self, + rows: &mut [Row], + register_to_row: &FxHashMap<(usize, Option), usize>, + column: usize, + col: &ComponentColumn, + ) -> usize { + let mut col_width = 0; + for op in &col.components { + let target_rows = get_row_indexes(op, register_to_row, true); + let control_rows = get_row_indexes(op, register_to_row, false); + + let mut all_rows = target_rows.clone(); + all_rows.extend(control_rows.iter()); + all_rows.sort_unstable(); + + // We'll need to know the entire range of rows for this operation so we can + // figure out the starting column and also so we can draw any + // vertical lines that cross wires. + let (begin, end) = all_rows.split_first().map_or((0, 0), |(first, tail)| { + (*first, tail.last().unwrap_or(first) + 1) + }); - add_operation_to_rows(op, rows, &targets, &controls, column, begin, end); + if op.children().is_empty() { + add_operation_to_rows(op, rows, &target_rows, &control_rows, column, begin, end); + col_width = max(col_width, 1); + } else { + let offset = self.add_boxed_group( + rows, + register_to_row, + &all_rows, + column, + op, + op.children(), + ); + col_width = max(col_width, offset); } } + + col_width + } + + fn add_boxed_group( + &self, + rows: &mut [Row], + register_to_row: &FxHashMap<(usize, Option), usize>, + target_rows: &[usize], + column: usize, + op: &Operation, + children: &Vec, + ) -> usize { + assert!( + !op.children().is_empty(), + "must only be called for an operation with children" + ); + assert!( + !op.is_controlled(), + "rendering controlled boxes not supported" + ); + assert!( + !op.is_measurement(), + "rendering measurement boxes not supported" + ); + + let mut offset = 0; + if self.render_groups { + add_box_start(op, rows, target_rows, column); + offset += 1; + } + + offset += self.add_grid(column + offset, children, rows, register_to_row); + + if self.render_groups { + add_box_end(op, rows, target_rows, column + offset); + offset += 1; + } + offset } } @@ -713,7 +894,7 @@ fn add_operation_to_rows( &operation.gate(), &operation.args(), operation.is_adjoint(), - operation.source().as_ref(), + operation.source_location(), ); } } @@ -722,7 +903,7 @@ fn add_operation_to_rows( for i in controls { let row = &mut rows[*i]; if matches!(row.wire, Wire::Qubit { .. }) && operation.is_measurement() { - row.add_measurement(column, operation.source().as_ref()); + row.add_measurement(column, operation.source_location()); } else { row.add_object(column, "●"); } @@ -744,6 +925,43 @@ fn add_operation_to_rows( } } +fn add_box_start(operation: &Operation, rows: &mut [Row], target_rows: &[usize], column: usize) { + assert!( + !operation.children().is_empty(), + "must only be called for an operation with children" + ); + + let mut first = true; + + for i in target_rows { + if first { + first = false; + rows[*i].add_object( + column, + format!( + "[ [{}{}]", + operation.gate(), + if operation.is_adjoint() { "'" } else { "" }, + ) + .as_str(), + ); + } else { + rows[*i].add_object(column, "["); + } + } +} + +fn add_box_end(operation: &Operation, rows: &mut [Row], target_rows: &[usize], column: usize) { + assert!( + !operation.children().is_empty(), + "must only be called for an operation with children" + ); + + for i in target_rows { + rows[*i].add_object(column, "]"); + } +} + /// Finalizes the columns by calculating their widths. fn finalize_columns(rows: &[Row]) -> Vec { // Find the end column for the whole circuit so that @@ -757,7 +975,7 @@ fn finalize_columns(rows: &[Row]) -> Vec { .iter() .map(|r| { if let Wire::Qubit { label } = &r.wire { - label.len() + label.len() + 1 } else { 0 } diff --git a/source/compiler/qsc_circuit/src/circuit/tests.rs b/source/compiler/qsc_circuit/src/circuit/tests.rs index 446785cc53..249c6425f3 100644 --- a/source/compiler/qsc_circuit/src/circuit/tests.rs +++ b/source/compiler/qsc_circuit/src/circuit/tests.rs @@ -53,7 +53,7 @@ fn measurement(q_id: usize, c_id: usize) -> Operation { qubits: vec![Register::quantum(q_id)], results: vec![Register::classical(q_id, c_id)], children: vec![], - source: None, + metadata: None, }) } @@ -65,7 +65,7 @@ fn unitary(gate: &str, targets: Vec) -> Operation { controls: vec![], targets, children: vec![], - source: None, + metadata: None, }) } @@ -77,7 +77,7 @@ fn ctl_unitary(gate: &str, targets: Vec, controls: Vec) -> O controls, targets, children: vec![], - source: None, + metadata: None, }) } @@ -214,7 +214,7 @@ fn with_args() { controls: vec![], targets: vec![Register::quantum(0)], children: vec![], - source: None, + metadata: None, })]]), }; @@ -235,7 +235,7 @@ fn two_targets() { controls: vec![], targets: vec![Register::quantum(0), Register::quantum(2)], children: vec![], - source: None, + metadata: None, })]]), }; diff --git a/source/npm/qsharp/src/data-structures/circuit.ts b/source/npm/qsharp/src/data-structures/circuit.ts index e1a9afe63a..5bcbb7fdee 100644 --- a/source/npm/qsharp/src/data-structures/circuit.ts +++ b/source/npm/qsharp/src/data-structures/circuit.ts @@ -120,7 +120,9 @@ export interface BaseOperation { isConditional?: boolean; /** Specify conditions on when to render operation. */ conditionalRender?: ConditionalRender; - source?: SourceLocation; + + /** Not written to file */ + metadata?: Metadata; } /** @@ -279,3 +281,8 @@ export interface SourceLocation { line: number; column: number; } + +export interface Metadata { + source?: SourceLocation; + scopeLocation?: SourceLocation; +} diff --git a/source/npm/qsharp/test/circuits-cases/bell-pair.qs.snapshot.html b/source/npm/qsharp/test/circuits-cases/bell-pair.qs.snapshot.html index 1c2a9a68a8..f86eb8f357 100644 --- a/source/npm/qsharp/test/circuits-cases/bell-pair.qs.snapshot.html +++ b/source/npm/qsharp/test/circuits-cases/bell-pair.qs.snapshot.html @@ -8,7 +8,319 @@ -
+

circuit-eval-collapsed

+ +

circuit-eval-expanded

+
bell-pair.qs:8:5 H(q1); @@ -148,8 +460,8 @@ bell-pair.qs:9:5 CNOT(q1, q2); bell-pair.qs:4:6 (MResetZ(q1), MResetZ(q2)) @@ -212,8 +524,8 @@ bell-pair.qs:4:19 (MResetZ(q1), MResetZ(q2)) @@ -246,8 +558,8 @@ bell-pair.qs:4:6 (MResetZ(q1), MResetZ(q2)) @@ -277,8 +589,8 @@ bell-pair.qs:4:19 (MResetZ(q1), MResetZ(q2)) diff --git a/source/npm/qsharp/test/circuits-cases/functors.qs b/source/npm/qsharp/test/circuits-cases/functors.qs new file mode 100644 index 0000000000..a795a23840 --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/functors.qs @@ -0,0 +1,19 @@ +operation Main() : Result[] { + use q = Qubit(); + Foo(q); + Adjoint Foo(q); + [MResetZ(q)] +} + +operation Foo(q : Qubit) : Unit is Adj + Ctl { + + body (...) { + X(q); + } + + adjoint (...) { + Y(q); + } + + controlled (cs, ...) {} +} diff --git a/source/npm/qsharp/test/circuits-cases/functors.qs.snapshot.html b/source/npm/qsharp/test/circuits-cases/functors.qs.snapshot.html new file mode 100644 index 0000000000..ce22688a9b --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/functors.qs.snapshot.html @@ -0,0 +1,409 @@ + + + + + + + +

circuit-eval-collapsed

+
+ + + + functors.qs:2:5 use q = Qubit(); + + | + ψ + 0 + ⟩ + + + + + + + + + + + + + + + + + + + + + + Foo + + + + + + + + + + + + + + Foo + + + + + + + + + + + functors.qs:5:6 [MResetZ(q)] + + + + + + + + + + functors.qs:5:6 [MResetZ(q)] + + + + + + |0⟩ + + + + + + + + + + + + + +
+

circuit-eval-expanded

+ + + diff --git a/source/npm/qsharp/test/circuits-cases/intrinsics.qs b/source/npm/qsharp/test/circuits-cases/intrinsics.qs new file mode 100644 index 0000000000..084cd59eaa --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/intrinsics.qs @@ -0,0 +1,16 @@ +operation Main() : Result[] { + use q = Qubit(); + CustomIntrinsic(q); + SimulatableIntrinsic(q); + [MResetZ(q)] +} + +operation CustomIntrinsic(q : Qubit) : Unit { + body intrinsic; +} + + +@SimulatableIntrinsic() +operation SimulatableIntrinsic(q : Qubit) : Unit { + H(q); +} diff --git a/source/npm/qsharp/test/circuits-cases/intrinsics.qs.snapshot.html b/source/npm/qsharp/test/circuits-cases/intrinsics.qs.snapshot.html new file mode 100644 index 0000000000..296c84991e --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/intrinsics.qs.snapshot.html @@ -0,0 +1,423 @@ + + + + + + + +

circuit-eval-collapsed

+ +

circuit-eval-expanded

+ + + diff --git a/source/npm/qsharp/test/circuits-cases/lambda.qs b/source/npm/qsharp/test/circuits-cases/lambda.qs new file mode 100644 index 0000000000..184f202789 --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/lambda.qs @@ -0,0 +1,6 @@ +operation Main() : Result[] { + use q = Qubit(); + let lambda = (q => H(q)); + lambda(q); + [MResetZ(q)] +} diff --git a/source/npm/qsharp/test/circuits-cases/lambda.qs.snapshot.html b/source/npm/qsharp/test/circuits-cases/lambda.qs.snapshot.html new file mode 100644 index 0000000000..9f2e72cebb --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/lambda.qs.snapshot.html @@ -0,0 +1,363 @@ + + + + + + + +

circuit-eval-collapsed

+ +

circuit-eval-expanded

+ + + diff --git a/source/npm/qsharp/test/circuits-cases/nested-callables.qs b/source/npm/qsharp/test/circuits-cases/nested-callables.qs new file mode 100644 index 0000000000..2db05695fe --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/nested-callables.qs @@ -0,0 +1,20 @@ +operation Main() : Unit { + use q1 = Qubit(); + use q2 = Qubit(); + Bar(q1); + Bar(q2); + Foo(q1); + Bar(q1); + Foo(q1); + Foo(q2); + Bar(q2); + Foo(q2); +} +operation Foo(q : Qubit) : Unit { + Bar(q); + MResetZ(q); +} +operation Bar(q : Qubit) : Unit { + X(q); + Y(q); +} diff --git a/source/npm/qsharp/test/circuits-cases/nested-callables.qs.snapshot.html b/source/npm/qsharp/test/circuits-cases/nested-callables.qs.snapshot.html new file mode 100644 index 0000000000..54a876a04c --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/nested-callables.qs.snapshot.html @@ -0,0 +1,1107 @@ + + + + + + + +

circuit-eval-collapsed

+
+ + + + nested-callables.qs:2:5 use q1 = Qubit(); + + | + ψ + 0 + ⟩ + + + + nested-callables.qs:3:5 use q2 = Qubit(); + + | + ψ + 1 + ⟩ + + + + + + + + + + + + + + + + + Bar + + + + + + + + + + + + + + Bar + + + + + + + + + + + + + + Foo + + + + + + + + + + + + + + Foo + + + + + + + + + + + + + + Bar + + + + + + + + + + + + + + Bar + + + + + + + + + + + + + + Foo + + + + + + + + + + + + + + Foo + + + + + + + + + + + + + + + + +
+

circuit-eval-expanded

+
+ + + + nested-callables.qs:2:5 use q1 = Qubit(); + + | + ψ + 0 + ⟩ + + + + nested-callables.qs:3:5 use q2 = Qubit(); + + | + ψ + 1 + ⟩ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nested-callables.qs:18:5 X(q); + + + + + + + + + + nested-callables.qs:18:5 X(q); + + + + + + + + + + nested-callables.qs:19:5 Y(q); + + + + + + Y + + + + + + + nested-callables.qs:19:5 Y(q); + + + + + + Y + + + + + + + nested-callables.qs:18:5 X(q); + + + + + + + + + + nested-callables.qs:18:5 X(q); + + + + + + + + + + nested-callables.qs:19:5 Y(q); + + + + + + Y + + + + + + + nested-callables.qs:19:5 Y(q); + + + + + + Y + + + + + + + nested-callables.qs:15:5 MResetZ(q); + + + + + + + + + + nested-callables.qs:15:5 MResetZ(q); + + + + + + + + + + nested-callables.qs:15:5 MResetZ(q); + + + + + + |0⟩ + + + + + + + nested-callables.qs:15:5 MResetZ(q); + + + + + + |0⟩ + + + + + + + nested-callables.qs:18:5 X(q); + + + + + + + + + + nested-callables.qs:18:5 X(q); + + + + + + + + + + nested-callables.qs:19:5 Y(q); + + + + + + Y + + + + + + + nested-callables.qs:19:5 Y(q); + + + + + + Y + + + + + + + nested-callables.qs:18:5 X(q); + + + + + + + + + + nested-callables.qs:18:5 X(q); + + + + + + + + + + nested-callables.qs:19:5 Y(q); + + + + + + Y + + + + + + + nested-callables.qs:19:5 Y(q); + + + + + + Y + + + + + + + nested-callables.qs:15:5 MResetZ(q); + + + + + + + + + + nested-callables.qs:15:5 MResetZ(q); + + + + + + + + + + nested-callables.qs:15:5 MResetZ(q); + + + + + + |0⟩ + + + + + + + nested-callables.qs:15:5 MResetZ(q); + + + + + + |0⟩ + + + + + + + +
+ + diff --git a/source/npm/qsharp/test/circuits-cases/ops-with-gap-ranges.qs b/source/npm/qsharp/test/circuits-cases/ops-with-gap-ranges.qs new file mode 100644 index 0000000000..42e0ad5edf --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/ops-with-gap-ranges.qs @@ -0,0 +1,16 @@ +operation Main() : Unit { + use qs = Qubit[10]; + Foo(qs); + Bar(qs); + MResetEachZ(qs); +} + +operation Foo(qs : Qubit[]) : Unit { + Rxx(1.0, qs[0], qs[2]); + Rxx(1.0, qs[4], qs[6]); +} + +operation Bar(qs : Qubit[]) : Unit { + Rxx(1.0, qs[1], qs[3]); + Rxx(1.0, qs[5], qs[9]); +} diff --git a/source/npm/qsharp/test/circuits-cases/ops-with-gap-ranges.qs.snapshot.html b/source/npm/qsharp/test/circuits-cases/ops-with-gap-ranges.qs.snapshot.html new file mode 100644 index 0000000000..ae2c12af59 --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/ops-with-gap-ranges.qs.snapshot.html @@ -0,0 +1,2869 @@ + + + + + + + +

circuit-eval-collapsed

+
+ + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 0 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 1 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 2 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 3 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 4 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 5 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 6 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 7 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 8 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 9 + ⟩ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Foo + + + + + + Foo + + + + + + Foo + + + + + + Foo + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + + + + + + Bar + + + + + + Bar + + + + + + Bar + + + + + + Bar + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + + + + + + + +
+

circuit-eval-expanded

+
+ + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 0 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 1 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 2 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 3 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 4 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 5 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 6 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 7 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 8 + ⟩ + + + + ops-with-gap-ranges.qs:2:5 use qs = Qubit[10]; + + | + ψ + 9 + ⟩ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ops-with-gap-ranges.qs:9:5 Rxx(1.0, qs[0], qs[2]); + + + + + + + Rxx + + + 1.0000 + + + + + + Rxx + + + 1.0000 + + + + + + + ops-with-gap-ranges.qs:10:5 Rxx(1.0, qs[4], qs[6]); + + + + + + + Rxx + + + 1.0000 + + + + + + Rxx + + + 1.0000 + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:14:5 Rxx(1.0, qs[1], qs[3]); + + + + + + + Rxx + + + 1.0000 + + + + + + Rxx + + + 1.0000 + + + + + + + ops-with-gap-ranges.qs:15:5 Rxx(1.0, qs[5], qs[9]); + + + + + + + Rxx + + + 1.0000 + + + + + + Rxx + + + 1.0000 + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + ops-with-gap-ranges.qs:5:5 MResetEachZ(qs); + + + + + + |0⟩ + + + + + + + +
+ + diff --git a/source/npm/qsharp/test/circuits-cases/qubit-reuse.qs.snapshot.html b/source/npm/qsharp/test/circuits-cases/qubit-reuse.qs.snapshot.html index 897d158da9..f499fdb47e 100644 --- a/source/npm/qsharp/test/circuits-cases/qubit-reuse.qs.snapshot.html +++ b/source/npm/qsharp/test/circuits-cases/qubit-reuse.qs.snapshot.html @@ -8,7 +8,301 @@ -
+

circuit-eval-collapsed

+ +

circuit-eval-expanded

+
qubit-reuse.qs:4:9 X(q1); @@ -122,8 +416,8 @@ qubit-reuse.qs:5:9 MResetZ(q1); @@ -156,8 +450,8 @@ qubit-reuse.qs:5:9 MResetZ(q1); @@ -187,8 +481,8 @@ qubit-reuse.qs:9:9 Y(q2); @@ -213,8 +507,8 @@ qubit-reuse.qs:10:9 MResetZ(q2); @@ -247,8 +541,8 @@ qubit-reuse.qs:10:9 MResetZ(q2); diff --git a/source/npm/qsharp/test/circuits-cases/two-qubit-gates.qs.snapshot.html b/source/npm/qsharp/test/circuits-cases/two-qubit-gates.qs.snapshot.html index bf96ff57a4..07b858fd58 100644 --- a/source/npm/qsharp/test/circuits-cases/two-qubit-gates.qs.snapshot.html +++ b/source/npm/qsharp/test/circuits-cases/two-qubit-gates.qs.snapshot.html @@ -8,7 +8,370 @@ -
+

circuit-eval-collapsed

+ +

circuit-eval-expanded

+
two-qubit-gates.qs:3:5 H(q2); @@ -145,8 +508,8 @@ two-qubit-gates.qs:4:5 Rzz(1.2345, q1, q3); @@ -218,8 +581,8 @@ two-qubit-gates.qs:5:5 CNOT(q2, q3); two-qubit-gates.qs:6:5 SWAP(q1, q2); @@ -282,8 +645,8 @@ two-qubit-gates.qs:7:5 MResetEachZ([q3]) @@ -316,8 +679,8 @@ two-qubit-gates.qs:7:5 MResetEachZ([q3]) diff --git a/source/npm/qsharp/test/circuits.js b/source/npm/qsharp/test/circuits.js index 7cf68c7a65..3d70fb409a 100644 --- a/source/npm/qsharp/test/circuits.js +++ b/source/npm/qsharp/test/circuits.js @@ -230,36 +230,71 @@ test("circuit snapshot tests - .qs files", async (t) => { const relName = path.basename(file); await t.test(`${relName}`, async (tt) => { const circuitSource = fs.readFileSync(file, "utf8"); - const compiler = getCompiler(); - const container = createContainerElement(`circuit`); - try { - // Generate the circuit from Q# - const circuit = await compiler.getCircuit( - { - sources: [[relName, circuitSource]], - languageFeatures: [], - profile: "adaptive_rif", - }, - { - generationMethod: "classicalEval", - maxOperations: 100, - sourceLocations: true, - }, - ); + await generateAndDrawCircuit( + relName, + circuitSource, + "circuit-eval-collapsed", + "classicalEval", + 0, + ); - // Render the circuit - draw(circuit, container, { - renderLocations, - }); - } catch (e) { - const pre = document.createElement("pre"); - pre.appendChild( - document.createTextNode(`Error generating circuit: ${e}`), - ); - container.appendChild(pre); - } + await generateAndDrawCircuit( + relName, + circuitSource, + "circuit-eval-expanded", + "classicalEval", + 999999, + ); await checkDocumentSnapshot(tt, tt.name); }); } }); + +/** + * @param {string} name + * @param {string} circuitSource + * @param {string} id + * @param { "classicalEval" | "simulate"} generationMethod + * @param {number} renderDepth + */ +async function generateAndDrawCircuit( + name, + circuitSource, + id, + generationMethod, + renderDepth, +) { + const compiler = getCompiler(); + const title = document.createElement("div"); + title.innerHTML = `

${id}

`; + document.body.appendChild(title); + const container = createContainerElement(id); + try { + // Generate the circuit from Q# + const circuit = await compiler.getCircuit( + { + sources: [[name, circuitSource]], + languageFeatures: [], + profile: "adaptive_rif", + }, + { + generationMethod, + groupByScope: true, + maxOperations: 100, + sourceLocations: true, + }, + undefined, + ); + + // Render the circuit + draw(circuit, container, { + renderDepth, + renderLocations, + }); + } catch (e) { + const pre = document.createElement("pre"); + pre.appendChild(document.createTextNode(`Error generating circuit: ${e}`)); + container.appendChild(pre); + } +} diff --git a/source/npm/qsharp/ux/circuit-vis/formatters/gateFormatter.ts b/source/npm/qsharp/ux/circuit-vis/formatters/gateFormatter.ts index 04e27564bb..8f43a897ac 100644 --- a/source/npm/qsharp/ux/circuit-vis/formatters/gateFormatter.ts +++ b/source/npm/qsharp/ux/circuit-vis/formatters/gateFormatter.ts @@ -164,7 +164,7 @@ const _zoomButton = ( let { dataAttributes } = renderData; dataAttributes = dataAttributes || {}; - const expanded = "expanded" in dataAttributes; + const expanded = dataAttributes["expanded"] == "true"; const x = gateBoundingBoxX + 2; const y = gateBoundingBoxY + 2; diff --git a/source/npm/qsharp/ux/circuit-vis/process.ts b/source/npm/qsharp/ux/circuit-vis/process.ts index 35bb3e464b..1e5bbd997c 100644 --- a/source/npm/qsharp/ux/circuit-vis/process.ts +++ b/source/npm/qsharp/ux/circuit-vis/process.ts @@ -213,7 +213,11 @@ const _opToRenderData = ( ), })) .filter((col) => col.components.length > 0); - let childrenInstrs = processOperations(onZeroOps, registers); + let childrenInstrs = processOperations( + onZeroOps, + registers, + renderLocations, + ); const zeroGates: GateRenderData[][] = childrenInstrs.renderDataArray; const zeroChildWidth: number = childrenInstrs.svgWidth; @@ -225,7 +229,7 @@ const _opToRenderData = ( ), })) .filter((col) => col.components.length > 0); - childrenInstrs = processOperations(onOneOps, registers); + childrenInstrs = processOperations(onOneOps, registers, renderLocations); const oneGates: GateRenderData[][] = childrenInstrs.renderDataArray; const oneChildWidth: number = childrenInstrs.svgWidth; @@ -247,7 +251,11 @@ const _opToRenderData = ( conditionalRender == ConditionalRender.AsGroup && (children?.length || 0) > 0 ) { - const childrenInstrs = processOperations(children!, registers); + const childrenInstrs = processOperations( + children!, + registers, + renderLocations, + ); renderData.type = GateType.Group; renderData.children = childrenInstrs.renderDataArray; // _zoomButton function in gateFormatter.ts relies on @@ -285,8 +293,8 @@ const _opToRenderData = ( // Set gate width renderData.width = getGateWidth(renderData); - if (op.source && renderLocations) { - renderData.link = renderLocations([op.source]); + if (op.metadata?.source && renderLocations) { + renderData.link = renderLocations([op.metadata.source]); } // Extend existing data attributes with user-provided data attributes diff --git a/source/npm/qsharp/ux/circuit-vis/sqore.ts b/source/npm/qsharp/ux/circuit-vis/sqore.ts index dfb2b3a858..2c1a85021b 100644 --- a/source/npm/qsharp/ux/circuit-vis/sqore.ts +++ b/source/npm/qsharp/ux/circuit-vis/sqore.ts @@ -59,6 +59,7 @@ export type DrawOptions = { export class Sqore { circuit: Circuit; gateRegistry: GateRegistry = {}; + renderDepth: number = this.options.renderDepth ?? 0; /** * Initializes Sqore object. * @@ -106,7 +107,6 @@ export class Sqore { // Create copy of circuit to prevent mutation const _circuit: Circuit = circuit ?? JSON.parse(JSON.stringify(this.circuit)); - const renderDepth = this.options.renderDepth || 0; // Assign unique locations to each operation _circuit.componentGrid.forEach((col, colIndex) => @@ -118,7 +118,7 @@ export class Sqore { // Render operations starting at given depth _circuit.componentGrid = this.selectOpsAtDepth( _circuit.componentGrid, - renderDepth, + this.renderDepth, ); // If only one top-level operation, expand automatically: @@ -129,7 +129,9 @@ export class Sqore { Object.prototype.hasOwnProperty.call( _circuit.componentGrid[0].components[0].dataAttributes, "location", - ) + ) && + _circuit.componentGrid[0].components[0].dataAttributes["expanded"] !== + "false" ) { const location: string = _circuit.componentGrid[0].components[0].dataAttributes["location"]; @@ -337,11 +339,11 @@ export class Sqore { } else { selectedCol.push(op); } - selectedOps.push({ components: selectedCol }); - if (extraCols.length > 0) { - selectedOps.push(...extraCols); - } }); + selectedOps.push({ components: selectedCol }); + if (extraCols.length > 0) { + selectedOps.push(...extraCols); + } }); return selectedOps; } @@ -478,7 +480,7 @@ export class Sqore { // Collapse parent gate and its children if (opId.startsWith(parentLoc)) { op.conditionalRender = ConditionalRender.Always; - delete op.dataAttributes["expanded"]; + op.dataAttributes["expanded"] = "false"; } }), ); diff --git a/source/pip/qsharp/_native.pyi b/source/pip/qsharp/_native.pyi index 60dda352d2..a3079d296b 100644 --- a/source/pip/qsharp/_native.pyi +++ b/source/pip/qsharp/_native.pyi @@ -420,6 +420,7 @@ class CircuitConfig: max_operations: Optional[int] = None, generation_method: Optional["CircuitGenerationMethod"] = None, source_locations: Optional[bool] = None, + group_by_scope: Optional[bool] = None, ) -> None: ... """ diff --git a/source/pip/qsharp/_qsharp.py b/source/pip/qsharp/_qsharp.py index ebb0bf12f2..95902e18af 100644 --- a/source/pip/qsharp/_qsharp.py +++ b/source/pip/qsharp/_qsharp.py @@ -837,6 +837,7 @@ def circuit( generation_method: Optional[CircuitGenerationMethod] = None, max_operations: Optional[int] = None, source_locations: bool = False, + group_by_scope: bool = False, ) -> Circuit: """ Synthesizes a circuit for a Q# program. Either an entry @@ -860,6 +861,7 @@ def circuit( max_operations=max_operations, generation_method=generation_method, source_locations=source_locations, + group_by_scope=group_by_scope, ) if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): diff --git a/source/pip/qsharp/openqasm/_circuit.py b/source/pip/qsharp/openqasm/_circuit.py index d8c5684476..939a57a154 100644 --- a/source/pip/qsharp/openqasm/_circuit.py +++ b/source/pip/qsharp/openqasm/_circuit.py @@ -48,10 +48,12 @@ def circuit( max_operations = kwargs.pop("max_operations", None) generation_method = kwargs.pop("generation_method", None) source_locations = kwargs.pop("source_locations", None) + group_by_scope = kwargs.pop("group_by_scope", None) config = CircuitConfig( max_operations=max_operations, generation_method=generation_method, source_locations=source_locations, + group_by_scope=group_by_scope, ) if isinstance(source, Callable) and hasattr(source, "__global_callable"): diff --git a/source/pip/src/interop.rs b/source/pip/src/interop.rs index 1a24e7b2f2..2fd279e75b 100644 --- a/source/pip/src/interop.rs +++ b/source/pip/src/interop.rs @@ -607,6 +607,9 @@ pub(crate) fn circuit_qasm_program( if let Some(locations) = config.source_locations { tracer_config.source_locations = locations; } + if let Some(group_by_scope) = config.group_by_scope { + tracer_config.group_by_scope = group_by_scope; + } let generation_method = if let Some(generation_method) = config.generation_method { generation_method.into() diff --git a/source/pip/src/interpreter.rs b/source/pip/src/interpreter.rs index a056cd4ef1..c61d213d30 100644 --- a/source/pip/src/interpreter.rs +++ b/source/pip/src/interpreter.rs @@ -805,6 +805,9 @@ impl Interpreter { if let Some(locations) = config.source_locations { tracer_config.source_locations = locations; } + if let Some(group_by_scope) = config.group_by_scope { + tracer_config.group_by_scope = group_by_scope; + } let generation_method = if let Some(generation_method) = config.generation_method { generation_method.into() @@ -1298,21 +1301,25 @@ pub(crate) struct CircuitConfig { pub(crate) generation_method: Option, #[pyo3(get, set)] pub(crate) source_locations: Option, + #[pyo3(get, set)] + pub(crate) group_by_scope: Option, } #[pymethods] impl CircuitConfig { #[new] - #[pyo3(signature=(*,max_operations=None, generation_method=None, source_locations=None))] + #[pyo3(signature=(*,max_operations=None, generation_method=None, source_locations=None, group_by_scope=None))] fn new( max_operations: Option, generation_method: Option, source_locations: Option, + group_by_scope: Option, ) -> Self { Self { max_operations, generation_method, source_locations, + group_by_scope, } } } diff --git a/source/samples_test/src/tests.rs b/source/samples_test/src/tests.rs index ccdc4b3746..4445fa4616 100644 --- a/source/samples_test/src/tests.rs +++ b/source/samples_test/src/tests.rs @@ -21,7 +21,9 @@ mod OpenQASM; mod OpenQASM_generated; use qsc::{ - LanguageFeatures, PackageType, SourceMap, TargetCapabilityFlags, compile, + LanguageFeatures, PackageType, SourceMap, TargetCapabilityFlags, + circuit::TracerConfig, + compile, hir::PackageId, interpret::{GenericReceiver, Interpreter}, packages::BuildableProgram, @@ -56,7 +58,11 @@ fn compile_and_run_internal(sources: SourceMap, debug: bool) -> String { LanguageFeatures::default(), store, &[(std_id, None)], - Default::default(), + TracerConfig { + group_by_scope: false, + source_locations: false, + max_operations: 0, + }, ) } else { Interpreter::new( diff --git a/source/vscode/package.json b/source/vscode/package.json index 20340c7c67..a673d9e920 100644 --- a/source/vscode/package.json +++ b/source/vscode/package.json @@ -112,6 +112,14 @@ "minimum": 1, "description": "The maximum number of operations to include in the circuit diagram." }, + "groupByScope": { + "type": "boolean", + "default": false, + "tags": [ + "experimental" + ], + "description": "(Experimental) Whether to group operations into scopes based on the original source code." + }, "generationMethod": { "type": "string", "default": "classicalEval", diff --git a/source/vscode/src/azure/commands.ts b/source/vscode/src/azure/commands.ts index 7abffe1407..90bc0cde72 100644 --- a/source/vscode/src/azure/commands.ts +++ b/source/vscode/src/azure/commands.ts @@ -45,9 +45,11 @@ import { const workspacesSecret = `${qsharpExtensionId}.workspaces`; let extensionUri: vscode.Uri; +let prerelease: boolean; export async function initAzureWorkspaces(context: vscode.ExtensionContext) { extensionUri = context.extensionUri; + prerelease = context.extension.id.includes("-dev"); const workspaceTreeProvider = new WorkspaceTreeProvider(); WorkspaceTreeProvider.instance = workspaceTreeProvider; @@ -694,7 +696,7 @@ async function getCircuitJson(program: FullProgramConfig): Promise { { program, }, - getConfig(), + getConfig(prerelease), 5000, // If we can't generate in 5 seconds, give up - something's wrong or program is way too complex ); diff --git a/source/vscode/src/circuit.ts b/source/vscode/src/circuit.ts index c59b4d6878..0b51bcbc65 100644 --- a/source/vscode/src/circuit.ts +++ b/source/vscode/src/circuit.ts @@ -58,6 +58,7 @@ export type CircuitOrError = { export async function showCircuitCommand( extensionUri: Uri, + prerelease: boolean, operation: IOperationInfo | undefined, telemetryInvocationType: UserTaskInvocationType, telemetryDocumentType?: QsharpDocumentType, @@ -76,7 +77,7 @@ export async function showCircuitCommand( {}, ); - const circuitConfig = getConfig(); + const circuitConfig = getConfig(prerelease); if (!programConfig) { const program = await getActiveProgram({ showModalError: true }); if (!program.success) { @@ -305,9 +306,10 @@ async function getCircuitOrError( } } -export function getConfig() { +export function getConfig(prerelease: boolean) { const defaultConfig = { maxOperations: 10001, + groupByScope: prerelease ? true : false, generationMethod: "classicalEval" as const, sourceLocations: true, }; @@ -321,6 +323,10 @@ export function getConfig() { "maxOperations" in config && typeof config.maxOperations === "number" ? config.maxOperations : defaultConfig.maxOperations, + groupByScope: + "groupByScope" in config && typeof config.groupByScope === "boolean" + ? config.groupByScope + : defaultConfig.groupByScope, generationMethod: "generationMethod" in config && typeof config.generationMethod === "string" && @@ -401,7 +407,9 @@ export function updateCircuitPanel( ) { const panelId = params?.operation?.operation || projectName; const title = params?.operation - ? `${params.operation.operation} with ${params.operation.totalNumQubits} input qubits` + ? params.operation.totalNumQubits > 0 + ? `${params.operation.operation} with ${params.operation.totalNumQubits} input qubits` + : params.operation.operation : projectName; const target = `Target profile: ${getTargetFriendlyName(targetProfile)} `; diff --git a/source/vscode/src/extension.ts b/source/vscode/src/extension.ts index 58bd4332e9..fe3f2e4de9 100644 --- a/source/vscode/src/extension.ts +++ b/source/vscode/src/extension.ts @@ -207,14 +207,14 @@ export class QsTextDocumentContentProvider } } +const qdkExtensionNameDev = "quantum.qsharp-lang-vscode-dev"; function checkForOldQdk() { const oldQdkExtension = vscode.extensions.getExtension( "quantum.quantum-devkit-vscode", ); - const prereleaseQdkExtension = vscode.extensions.getExtension( - "quantum.qsharp-lang-vscode-dev", - ); + const prereleaseQdkExtension = + vscode.extensions.getExtension(qdkExtensionNameDev); const releaseQdkExtension = vscode.extensions.getExtension( "quantum.qsharp-lang-vscode", diff --git a/source/vscode/src/gh-copilot/qsharpTools.ts b/source/vscode/src/gh-copilot/qsharpTools.ts index 46e9335153..061bd67ab2 100644 --- a/source/vscode/src/gh-copilot/qsharpTools.ts +++ b/source/vscode/src/gh-copilot/qsharpTools.ts @@ -46,7 +46,10 @@ type RunProgramResult = ProjectInfo & ); export class QSharpTools { - constructor(private extensionUri: vscode.Uri) {} + constructor( + private extensionUri: vscode.Uri, + private prerelease: boolean, + ) {} /** * Implements the `qdk-run-program` tool call. @@ -169,6 +172,7 @@ export class QSharpTools { const circuitOrError = await showCircuitCommand( this.extensionUri, + this.prerelease, undefined, UserTaskInvocationType.ChatToolCall, program.telemetryDocumentType, diff --git a/source/vscode/src/gh-copilot/tools.ts b/source/vscode/src/gh-copilot/tools.ts index 3a58a7ae73..4bfdec701c 100644 --- a/source/vscode/src/gh-copilot/tools.ts +++ b/source/vscode/src/gh-copilot/tools.ts @@ -104,7 +104,10 @@ const toolDefinitions: { ]; export function registerLanguageModelTools(context: vscode.ExtensionContext) { - qsharpTools = new QSharpTools(context.extensionUri); + qsharpTools = new QSharpTools( + context.extensionUri, + context.extension.id.includes("-dev"), + ); for (const { name, tool: fn, confirm: confirmFn } of toolDefinitions) { context.subscriptions.push( vscode.lm.registerTool(name, tool(context, name, fn, confirmFn)), diff --git a/source/vscode/src/webviewPanel.ts b/source/vscode/src/webviewPanel.ts index 7c6cfe9135..d19f7a7ca3 100644 --- a/source/vscode/src/webviewPanel.ts +++ b/source/vscode/src/webviewPanel.ts @@ -187,6 +187,7 @@ export function registerWebViewCommands(context: ExtensionContext) { async (resource?: vscode.Uri, operation?: IOperationInfo) => { await showCircuitCommand( context.extensionUri, + context.extension.id.includes("-dev"), operation, UserTaskInvocationType.Command, ); diff --git a/source/wasm/src/lib.rs b/source/wasm/src/lib.rs index 44bb9425ba..00f72c3d4a 100644 --- a/source/wasm/src/lib.rs +++ b/source/wasm/src/lib.rs @@ -146,14 +146,16 @@ pub(crate) fn get_estimates_from_openqasm( serializable_type! { CircuitConfig, { - max_operations: usize, generation_method: String, + max_operations: usize, source_locations: bool, + group_by_scope: bool, }, r#"export interface ICircuitConfig { + generationMethod: "simulate" | "classicalEval"; maxOperations: number; - generationMethod: "simulate" | "classicalEval" ; sourceLocations: boolean; + groupByScope: boolean; }"#, ICircuitConfig } @@ -178,6 +180,7 @@ pub fn get_circuit( let tracer_config = qsc::circuit::TracerConfig { source_locations: config.source_locations, max_operations: config.max_operations, + group_by_scope: config.group_by_scope, }; if is_openqasm_program(&program) {