diff --git a/source/compiler/qsc/src/interpret.rs b/source/compiler/qsc/src/interpret.rs index b2bd8abaa7..e4001b1140 100644 --- a/source/compiler/qsc/src/interpret.rs +++ b/source/compiler/qsc/src/interpret.rs @@ -848,7 +848,7 @@ impl Interpreter { self.circuit_tracer .as_ref() .expect("to call get_circuit, the interpreter should be initialized with circuit tracing enabled") - .snapshot(self.compiler.package_store()) + .snapshot(&(self.compiler.package_store(), &self.fir_store)) } /// Performs QIR codegen using the given entry expression on a new instance of the environment @@ -1022,7 +1022,7 @@ impl Interpreter { } } } - let circuit = tracer.finish(self.compiler.package_store()); + let circuit = tracer.finish(&(self.compiler.package_store(), &self.fir_store)); Ok(circuit) } diff --git a/source/compiler/qsc/src/interpret/circuit_tests.rs b/source/compiler/qsc/src/interpret/circuit_tests.rs index 8eef64cd42..5b61c557be 100644 --- a/source/compiler/qsc/src/interpret/circuit_tests.rs +++ b/source/compiler/qsc/src/interpret/circuit_tests.rs @@ -195,9 +195,7 @@ fn grouping_nested_callables() { @EntryPoint() operation Main() : Unit { use q = Qubit(); - for i in 0..5 { - Foo(q); - } + Foo(q); MResetZ(q); } @@ -219,31 +217,293 @@ fn grouping_nested_callables() { .expect("circuit generation should succeed"); expect![[r#" - q_0 ─ [ [Main] ─── [ [Foo] ─── H ──── H ──── H ──── H ──── H ──── H ──── ] ──── M ──── |0〉 ──── ] ── - [ ╘══════════════ ] ══ + q_0 ─ [ [Main] ─── [ [Foo] ─── H ──── ] ──── M ──── |0〉 ──── ] ── + [ ╘══════════════ ] ══ "#]] .assert_eq(&circ.display_with_groups().to_string()); } #[test] -fn classical_for_loop() { - let circ = circuit( +fn classical_for_loop_is_grouped() { + let circ = circuit_with_options( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + for i in 0..2 { + Foo(q); + } + } + + operation Foo(q: Qubit) : Unit { + X(q); + Y(q); + } + } + ", + Profile::Unrestricted, + CircuitEntryPoint::EntryPoint, + CircuitGenerationMethod::ClassicalEval, + TracerConfig { + max_operations: 1000, + source_locations: true, + group_by_scope: true, + prune_classical_qubits: false, + }, + ) + .expect("circuit generation should succeed"); + + let circ = circ.display_with_groups().to_string(); + + expect![[r#" + q_0@test.qs:4:20 ─ [ [Main] ─── [ [loop: 0..2@test.qs:5:20] ── [ [(1)@test.qs:5:34] ─── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ── Y@test.qs:12:20 ─── ] ──── ] ─── [ [(2)@test.qs:5:34] ─── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ── Y@test.qs:12:20 ─── ] ──── ] ─── [ [(3)@test.qs:5:34] ─── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ── Y@test.qs:12:20 ─── ] ──── ] ──── ] ──── ] ── + "#]] + .assert_eq(&circ); +} + +#[test] +fn dynamic_for_loop_is_grouped() { + let circ = circuit_with_options( + r" + operation Main() : Unit { + use qubit = Qubit(); + repeat { + H(qubit); + } until M(qubit) == Zero + fixup { + Reset(qubit); + } + } + ", + Profile::Unrestricted, + CircuitEntryPoint::EntryPoint, + CircuitGenerationMethod::Simulate, + TracerConfig { + max_operations: 1000, + source_locations: true, + group_by_scope: true, + prune_classical_qubits: false, + }, + ) + .expect("circuit generation should succeed"); + + let circ = circ.display_with_groups().to_string(); + + expect![[r#" + q_0@test.qs:2:15 ─ [ [Main] ─── [ [loop: M(qubit) == Zero@test.qs:3:16] ── [ [(1)@test.qs:3:23] ─── H@test.qs:4:20 ─── M@test.qs:5:24 ──── |0〉@test.qs:7:20 ───── ] ─── [ [(2)@test.qs:3:23] ─── H@test.qs:4:20 ─── M@test.qs:5:24 ──── |0〉@test.qs:7:20 ───── ] ─── [ [(3)@test.qs:3:23] ─── H@test.qs:4:20 ─── M@test.qs:5:24 ──── |0〉@test.qs:7:20 ───── ] ─── [ [(4)@test.qs:3:23] ─── H@test.qs:4:20 ─── M@test.qs:5:24 ──── ] ──── ] ──── ] ── + [ [ [ ╘══════════════════════════════════ ] ═══════════════════════════════════════════════════════╪════════════════════════════════════════════════════════════════════════════════════════════╪════════════════════════════════════════════════════════════════════════════════════════════╪══════════════════ ] ════ ] ══ + [ [ [ ╘══════════════════════════════════ ] ═══════════════════════════════════════════════════════╪════════════════════════════════════════════════════════════════════════════════════════════╪══════════════════ ] ════ ] ══ + [ [ [ ╘══════════════════════════════════ ] ═══════════════════════════════════════════════════════╪══════════════════ ] ════ ] ══ + [ [ [ ╘═══════════ ] ════ ] ════ ] ══ + "#]] + .assert_eq(&circ); +} + +#[test] +fn repeat_until_loop_is_grouped() { + let circ = circuit_with_options( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + mutable i = 0; + repeat { + Foo(q); + } until i == 2 + fixup { + set i += 1; + } + } + + operation Foo(q: Qubit) : Unit { + X(q); + Y(q); + } + } + ", + Profile::Unrestricted, + CircuitEntryPoint::EntryPoint, + CircuitGenerationMethod::ClassicalEval, + TracerConfig { + max_operations: 1000, + source_locations: true, + group_by_scope: true, + prune_classical_qubits: false, + }, + ) + .expect("circuit generation should succeed"); + + let circ = circ.display_with_groups().to_string(); + + expect![[r#" + q_0@test.qs:4:20 ─ [ [Main] ─── [ [loop: i == 2@test.qs:6:20] ── [ [(1)@test.qs:6:27] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:15:20 ── Y@test.qs:16:20 ─── ] ──── ] ─── [ [(2)@test.qs:6:27] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:15:20 ── Y@test.qs:16:20 ─── ] ──── ] ─── [ [(3)@test.qs:6:27] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:15:20 ── Y@test.qs:16:20 ─── ] ──── ] ──── ] ──── ] ── + "#]] + .assert_eq(&circ); +} + +#[test] +fn while_loop_is_grouped() { + let circ = circuit_with_options( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + mutable i = 0; + while (i < 2) { + Foo(q); + set i += 1; + } + } + + operation Foo(q: Qubit) : Unit { + X(q); + Y(q); + } + } + ", + Profile::Unrestricted, + CircuitEntryPoint::EntryPoint, + CircuitGenerationMethod::ClassicalEval, + TracerConfig { + max_operations: 1000, + source_locations: true, + group_by_scope: true, + prune_classical_qubits: false, + }, + ) + .expect("circuit generation should succeed"); + + let circ = circ.display_with_groups().to_string(); + + expect![[r#" + q_0@test.qs:4:20 ─ [ [Main] ─── [ [loop: i < 2@test.qs:6:20] ─── [ [(1)@test.qs:6:34] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:13:20 ── Y@test.qs:14:20 ─── ] ──── ] ─── [ [(2)@test.qs:6:34] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:13:20 ── Y@test.qs:14:20 ─── ] ──── ] ──── ] ──── ] ── + "#]] + .assert_eq(&circ); +} + +#[test] +fn loop_single_iteration_is_not_grouped() { + let circ = circuit_with_options( r" namespace Test { @EntryPoint() operation Main() : Unit { use q = Qubit(); + for i in 0..0 { + Foo(q); + } + } + + operation Foo(q: Qubit) : Unit { + X(q); + Y(q); + } + } + ", + Profile::Unrestricted, + CircuitEntryPoint::EntryPoint, + CircuitGenerationMethod::ClassicalEval, + TracerConfig { + max_operations: 1000, + source_locations: true, + group_by_scope: true, + prune_classical_qubits: false, + }, + ) + .expect("circuit generation should succeed"); + + let circ = circ.display_with_groups().to_string(); + + expect![[r#" + q_0@test.qs:4:20 ─ [ [Main] ─── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ── Y@test.qs:12:20 ─── ] ──── ] ── + "#]] + .assert_eq(&circ); +} + +#[test] +fn loop_vertical_is_not_grouped() { + let circ = circuit_with_options( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + use qs = Qubit[6]; for i in 0..5 { - X(q); + Foo(qs[i]); } } + + operation Foo(q: Qubit) : Unit { + X(q); + } } ", + Profile::Unrestricted, CircuitEntryPoint::EntryPoint, - ); + CircuitGenerationMethod::ClassicalEval, + TracerConfig { + max_operations: 1000, + source_locations: true, + group_by_scope: true, + prune_classical_qubits: false, + }, + ) + .expect("circuit generation should succeed"); + + let circ = circ.display_with_groups().to_string(); + + expect![[r#" + q_0@test.qs:4:20 ─ [ [Main] ─── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──── ] ── + q_1@test.qs:4:20 ───── [ ────── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──── ] ── + q_2@test.qs:4:20 ───── [ ────── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──── ] ── + q_3@test.qs:4:20 ───── [ ────── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──── ] ── + q_4@test.qs:4:20 ───── [ ────── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──── ] ── + q_5@test.qs:4:20 ───── [ ────── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──── ] ── + "#]] + .assert_eq(&circ); +} + +#[test] +fn for_loop_nested() { + let circ = circuit_with_options( + r" + namespace Test { + @EntryPoint() + operation Main() : Unit { + use qs = Qubit[3]; + for j in 0..2 { + for i in 0..2 { + Foo(qs[i]); + } + } + } + + operation Foo(q: Qubit) : Unit { + X(q); + } + } + ", + Profile::Unrestricted, + CircuitEntryPoint::EntryPoint, + CircuitGenerationMethod::ClassicalEval, + TracerConfig { + max_operations: 1000, + source_locations: true, + group_by_scope: true, + prune_classical_qubits: false, + }, + ) + .expect("circuit generation should succeed"); + + let circ = circ.display_with_groups().to_string(); expect![[r#" - q_0@test.qs:4:20 ─ X@test.qs:6:24 ─── X@test.qs:6:24 ─── X@test.qs:6:24 ─── X@test.qs:6:24 ─── X@test.qs:6:24 ─── X@test.qs:6:24 ── + q_0@test.qs:4:20 ─ [ [Main] ─── [ [loop: 0..2@test.qs:5:20] ── [ [(1)@test.qs:5:34] ─── [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──── ] ─── [ [(2)@test.qs:5:34] ─── [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──── ] ─── [ [(3)@test.qs:5:34] ─── [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──── ] ──── ] ──── ] ── + q_1@test.qs:4:20 ───── [ ─────────────────── [ ───────────────────────── [ ──────────── [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──── ] ───────────── [ ──────────── [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──── ] ───────────── [ ──────────── [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──── ] ──── ] ──── ] ── + q_2@test.qs:4:20 ───── [ ─────────────────── [ ───────────────────────── [ ──────────── [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──── ] ───────────── [ ──────────── [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──── ] ───────────── [ ──────────── [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──── ] ──── ] ──── ] ── "#]] .assert_eq(&circ); } diff --git a/source/compiler/qsc_circuit/src/builder.rs b/source/compiler/qsc_circuit/src/builder.rs index a09ea6c4cc..89c8702e7a 100644 --- a/source/compiler/qsc_circuit/src/builder.rs +++ b/source/compiler/qsc_circuit/src/builder.rs @@ -7,8 +7,8 @@ mod tests; use crate::{ ComponentColumn, circuit::{ - Circuit, Ket, Measurement, Metadata, Operation, PackageOffset, Qubit, Register, - ResolvedSourceLocation, SourceLocation, Unitary, operation_list_to_grid, + Circuit, Ket, Measurement, Metadata, Operation, Qubit, Register, SourceLocation, Unitary, + operation_list_to_grid, }, operations::QubitParam, }; @@ -18,19 +18,19 @@ use qsc_data_structures::{ line_column::{Encoding, Position}, }; use qsc_eval::{ + StackTrace, backend::Tracer, - debug::Frame, val::{self, Value}, }; -use qsc_fir::fir::{self, PackageId, StoreItemId}; -use qsc_frontend::compile::PackageStore; +use qsc_fir::fir::{self, ExprId, ExprKind, PackageId, PackageLookup, StoreItemId}; +use qsc_frontend::compile::{self}; 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}, + fmt::{Debug, Display, Write}, hash::Hash, - mem::replace, + mem::{replace, take}, rc::Rc, }; @@ -47,29 +47,29 @@ pub struct CircuitTracer { } impl Tracer for CircuitTracer { - fn qubit_allocate(&mut self, stack: &[Frame], q: usize) { + fn qubit_allocate(&mut self, stack: &StackTrace, q: usize) { let declared_at = self.user_code_call_location(stack); self.wire_map_builder.map_qubit(q, declared_at); } - fn qubit_release(&mut self, _stack: &[Frame], q: usize) { + fn qubit_release(&mut self, _stack: &StackTrace, q: usize) { self.wire_map_builder.unmap_qubit(q); } - fn qubit_swap_id(&mut self, _stack: &[Frame], q0: usize, q1: usize) { + fn qubit_swap_id(&mut self, _stack: &StackTrace, q0: usize, q1: usize) { self.wire_map_builder.swap(q0, q1); } fn gate( &mut self, - stack: &[Frame], + stack: &StackTrace, name: &str, is_adjoint: bool, targets: &[usize], controls: &[usize], theta: Option, ) { - let called_at = map_stack_frames_to_locations(stack); + let called_at = LogicalStack::from_evaluator_trace(stack); let display_args: Vec = theta.map(|p| format!("{p:.4}")).into_iter().collect(); let controls = if self.config.prune_classical_qubits { // Any controls that are known to be classically one can be removed, so this @@ -88,8 +88,8 @@ impl Tracer for CircuitTracer { ); } - fn measure(&mut self, stack: &[Frame], name: &str, q: usize, val: &val::Result) { - let called_at = map_stack_frames_to_locations(stack); + fn measure(&mut self, stack: &StackTrace, name: &str, q: usize, val: &val::Result) { + let called_at = LogicalStack::from_evaluator_trace(stack); let r = match val { val::Result::Id(id) => *id, val::Result::Loss | val::Result::Val(_) => { @@ -110,15 +110,15 @@ impl Tracer for CircuitTracer { } } - fn reset(&mut self, stack: &[Frame], q: usize) { - let called_at = map_stack_frames_to_locations(stack); + fn reset(&mut self, stack: &StackTrace, q: usize) { + let called_at = LogicalStack::from_evaluator_trace(stack); self.classical_one_qubits .remove(&self.wire_map_builder.wire_map.qubit_wire(q)); self.circuit_builder .reset(self.wire_map_builder.current(), q, called_at); } - fn custom_intrinsic(&mut self, stack: &[Frame], name: &str, arg: Value) { + fn custom_intrinsic(&mut self, stack: &StackTrace, name: &str, arg: Value) { // The qubit arguments are treated as the targets for custom gates. // Any remaining arguments will be kept in the display_args field // to be shown as part of the gate label when the circuit is rendered. @@ -142,7 +142,7 @@ impl Tracer for CircuitTracer { } else { vec![classical_args] }, - map_stack_frames_to_locations(stack), + LogicalStack::from_evaluator_trace(stack), ); } @@ -237,8 +237,8 @@ impl CircuitTracer { operations: &[OperationOrGroup], source_lookup: &impl SourceLookup, ) -> Circuit { - let mut operations: Vec = operations.to_vec(); - let mut qubits = self.wire_map_builder.wire_map.to_qubits(); + let mut operations = operations.to_vec(); + let mut qubits = self.wire_map_builder.wire_map.to_qubits(source_lookup); // We need to pass the original number of qubits, before any trimming, to finish the circuit below. let num_qubits = qubits.len(); @@ -250,12 +250,21 @@ impl CircuitTracer { operations.retain_mut(|op| self.should_keep_operation_mut(op)); } + if self.config.group_by_scope { + // Collapse unnecessary loop scopes + collapse_unnecessary_loop_scopes(&mut operations); + } + let operations = operations - .iter() - .map(|o| o.clone().into_operation(source_lookup)) + .into_iter() + .map(|o| o.into_operation(source_lookup)) .collect(); - finish_circuit(qubits, operations, num_qubits, source_lookup) + let component_grid = operation_list_to_grid(operations, num_qubits); + Circuit { + qubits, + component_grid, + } } fn should_keep_operation_mut(&self, op: &mut OperationOrGroup) -> bool { @@ -363,11 +372,21 @@ impl CircuitTracer { } } - fn user_code_call_location(&self, stack: &[Frame]) -> Option { - if !self.config.source_locations || stack.is_empty() || self.user_package_ids.is_empty() { - return None; + fn user_code_call_location(&self, stack: &StackTrace) -> Option { + if self.config.source_locations { + let logical_stack = LogicalStack::from_evaluator_trace(stack); + retain_user_frames(&self.user_package_ids, logical_stack) + .0 + .last() + .map(|l| { + let LogicalStackEntryLocation::Call(location) = *l.location() else { + panic!("last frame in stack trace should be a call to an intrinsic") + }; + location + }) + } else { + None } - first_user_code_location(&self.user_package_ids, stack) } fn mark_qubit_in_superposition(&mut self, wire: QubitWire) { @@ -478,51 +497,82 @@ impl CircuitTracer { } } -fn first_user_code_location( - user_package_ids: &[PackageId], - stack: &[Frame], -) -> Option { - for frame in stack.iter().rev() { - if user_package_ids.contains(&frame.id.package) { - return Some(PackageOffset { - package_id: frame.id.package, - offset: frame.span.lo, - }); +/// Removes any loop scopes that are unnecessary and replaces them with their children operations. +/// An unnecessary loop scope is one that either has a single child iteration, +/// or has multiple iterations that each operate on distinct sets of qubits (i.e. a "vertical" loop). +fn collapse_unnecessary_loop_scopes(operations: &mut Vec) { + let mut ops = vec![]; + for mut op in operations.drain(..) { + match &mut op.kind { + OperationOrGroupKind::Single => {} + OperationOrGroupKind::Group { children, .. } => { + collapse_unnecessary_loop_scopes(children); + } } - } - None + if let Some(children) = collapse_if_unnecessary(&mut op) { + ops.extend(children); + } else { + ops.push(op); + } + } + *operations = ops; } -fn finish_circuit( - mut qubits: Vec, - mut operations: Vec, - num_qubits: usize, - source_location_lookup: &impl SourceLookup, -) -> Circuit { - resolve_locations(&mut operations, source_location_lookup); - - for q in &mut qubits { - for source_location in &mut q.declarations { - resolve_source_location_if_unresolved(source_location, source_location_lookup); +/// If the given operation or group is an outer loop scope that can be collapsed, +/// returns its children operations or groups. +fn collapse_if_unnecessary(op: &mut OperationOrGroup) -> Option> { + if let OperationOrGroupKind::Group { + scope_stack, + children, + } = &mut op.kind + && let Scope::Loop(..) = scope_stack.current_lexical_scope() + { + if children.len() == 1 { + // remove the loop scope + let mut only_child = children.remove(0); + let OperationOrGroupKind::Group { children, .. } = &mut only_child.kind else { + panic!("only child of an outer loop scope should be a group"); + }; + return Some(take(children)); } - } - let component_grid = operation_list_to_grid(operations, num_qubits); - Circuit { - qubits, - component_grid, + // now, if each c applies to a distinct set of qubits, this loop is entirely vertical and can be collapsed as well + let mut distinct_sets_of_qubits = FxHashSet::default(); + for child_op in children.iter() { + let qs = child_op.all_qubits(); + if !distinct_sets_of_qubits.insert(qs) { + // There's overlap, so we won't collapse + return None; + } + } + let mut all_children = vec![]; + for mut child_op in children.drain(..) { + let OperationOrGroupKind::Group { children, .. } = &mut child_op.kind else { + panic!("only child of an outer loop scope should be a group"); + }; + all_children.extend(take(children)); + } + return Some(all_children); } + None } +/// Resolves structs that use compilation-specific IDs (`PackageId`s, `ExprId`s etc.) +/// to user legible names and source file locations. pub trait SourceLookup { - fn resolve_location(&self, package_offset: &PackageOffset) -> ResolvedSourceLocation; - fn resolve_scope(&self, scope: ScopeId) -> LexicalScope; + fn resolve_package_offset(&self, package_offset: &PackageOffset) -> SourceLocation; + fn resolve_scope(&self, scope: Scope) -> LexicalScope; + fn resolve_logical_stack_entry_location( + &self, + location: LogicalStackEntryLocation, + ) -> PackageOffset; } -impl SourceLookup for PackageStore { - fn resolve_location(&self, package_offset: &PackageOffset) -> ResolvedSourceLocation { +impl SourceLookup for (&compile::PackageStore, &fir::PackageStore) { + fn resolve_package_offset(&self, package_offset: &PackageOffset) -> SourceLocation { let package = self + .0 .get(map_fir_package_to_hir(package_offset.package_id)) .expect("package id must exist in store"); @@ -537,79 +587,133 @@ impl SourceLookup for PackageStore { package_offset.offset - source.offset, ); - ResolvedSourceLocation { + SourceLocation { file: source.name.to_string(), line: pos.line, 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(), - }; + fn resolve_scope(&self, scope_id: Scope) -> LexicalScope { + match scope_id { + Scope::Callable(store_item_id, functor_app) => { + let package = self + .0 + .get(map_fir_package_to_hir(store_item_id.package)) + .expect("package id must exist in store"); + + let item = package + .package + .items + .get(map_fir_local_item_to_hir(store_item_id.item)) + .expect("item id must exist in package"); + + let (scope_offset, scope_name) = match &item.kind { + hir::ItemKind::Callable(callable_decl) => { + let spec_decl = if functor_app.adjoint { + callable_decl.adj.as_ref().unwrap_or(&callable_decl.body) + } else { + &callable_decl.body + }; + + (spec_decl.span.lo, callable_decl.name.name.clone()) + } + _ => panic!("only callables should be in the stack"), + }; - 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 { + location: Some(PackageOffset { + package_id: store_item_id.package, + offset: scope_offset, + }), + name: scope_name, + is_adjoint: functor_app.adjoint, + } + } + Scope::Loop(package_id, expr_id) => { + let package_store = self.1; + let package = package_store.get(package_id); + let loop_expr = package.get_expr(expr_id); + let ExprKind::While(cond_expr_id, _) = &loop_expr.kind else { + panic!("only while loops should be in loop containers"); + }; + let cond_expr = package.get_expr(*cond_expr_id); + let expr_contents = self + .0 + .get(map_fir_package_to_hir(package_id)) + .and_then(|p| p.sources.find_by_offset(cond_expr.span.lo)) + .map(|s| { + s.contents[(cond_expr.span.lo - s.offset) as usize + ..(cond_expr.span.hi - s.offset) as usize] + .to_string() + }); - LexicalScope::Callable { - location: PackageOffset { - package_id: item_id.package, - offset: scope_start_offset, + LexicalScope { + name: format!("loop: {}", expr_contents.unwrap_or_default()).into(), + location: Some(PackageOffset { + package_id, + offset: loop_expr.span.lo, + }), + is_adjoint: false, + } + } + Scope::LoopIteration(package_id, expr_id, i) => { + let package_store = self.1; + let package = package_store.get(package_id); + let loop_expr = package.get_expr(expr_id); + let ExprKind::While(_cond, body) = &loop_expr.kind else { + panic!("only while loops should be used for loop body locations"); + }; + let block = package.get_block(*body); + + LexicalScope { + name: format!("({i})").into(), + location: Some(PackageOffset { + package_id, + offset: block.span.lo, + }), + is_adjoint: false, + } + } + Scope::Top => LexicalScope { + name: "top".into(), + location: None, + is_adjoint: false, }, - name: scope_name, - functor_app, } } -} -fn resolve_locations(operations: &mut [Operation], source_location_lookup: &impl SourceLookup) { - for op in operations { - let children_columns = op.children_mut(); - for column in children_columns { - resolve_locations(&mut column.components, source_location_lookup); - } + fn resolve_logical_stack_entry_location( + &self, + location: LogicalStackEntryLocation, + ) -> PackageOffset { + match location { + LogicalStackEntryLocation::Call(package_offset) => package_offset, + LogicalStackEntryLocation::Loop(package_id, loop_expr_id) => { + let fir_package_store = self.1; + let package = fir_package_store.get(package_id); + let expr = package.get_expr(loop_expr_id); - if let Some(source) = op.source_location_mut() { - resolve_source_location_if_unresolved(source, source_location_lookup); - } + PackageOffset { + package_id, + offset: expr.span.lo, + } + } + LogicalStackEntryLocation::LoopIteration(package_id, expr, _i) => { + let fir_package_store = self.1; + let package = fir_package_store.get(package_id); + let expr = package.get_expr(expr); + let ExprKind::While(_cond, body) = &expr.kind else { + panic!("only while loops should be used for loop body locations"); + }; - if let Some(source) = op.scope_location_mut() { - resolve_source_location_if_unresolved(source, source_location_lookup); - } - } -} + let block = package.get_block(*body); -fn resolve_source_location_if_unresolved( - source_location: &mut SourceLocation, - source_location_lookup: &impl SourceLookup, -) { - match source_location { - SourceLocation::Resolved(_) => {} - SourceLocation::Unresolved(package_offset) => { - let resolved_source_location = source_location_lookup.resolve_location(package_offset); - *source_location = SourceLocation::Resolved(resolved_source_location); + PackageOffset { + package_id, + offset: block.span.lo, + } + } } } } @@ -677,7 +781,7 @@ impl WireMap { .expect("result should already be mapped") } - fn to_qubits(&self) -> Vec { + fn to_qubits(&self, source_lookup: &impl SourceLookup) -> Vec { let mut qubits = vec![]; for (QubitWire(wire_id), (declarations, results)) in self.qubit_wires.iter() { qubits.push(Qubit { @@ -685,7 +789,7 @@ impl WireMap { num_results: results.len(), declarations: declarations .iter() - .map(|span| SourceLocation::Unresolved(*span)) + .map(|offset| source_lookup.resolve_package_offset(offset)) .collect(), }); } @@ -784,28 +888,14 @@ impl WireMapBuilder { } } -#[derive(Clone, Debug)] +#[derive(Clone)] struct OperationOrGroup { kind: OperationOrGroupKind, + location: Option, 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)] +#[derive(Clone)] enum OperationOrGroupKind { Single, Group { @@ -819,6 +909,7 @@ impl OperationOrGroup { Self { kind: OperationOrGroupKind::Single, op, + location: None, } } @@ -976,6 +1067,7 @@ impl OperationOrGroup { scope_location: None, }), }), + location: None, } } @@ -1041,13 +1133,14 @@ impl OperationOrGroup { } } - fn set_location(&mut self, location: PackageOffset) { - self.op - .source_location_mut() - .replace(SourceLocation::Unresolved(location)); - } + fn into_operation(mut self, source_lookup: &impl SourceLookup) -> Operation { + if let Some(location) = self.location { + let package_offset = source_lookup.resolve_logical_stack_entry_location(location); + let location = source_lookup.resolve_package_offset(&package_offset); + + self.op.source_location_mut().replace(location); + } - fn into_operation(mut self, scope_resolver: &impl SourceLookup) -> Operation { match self.kind { OperationOrGroupKind::Single => self.op, OperationOrGroupKind::Group { @@ -1057,11 +1150,12 @@ impl OperationOrGroup { 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); + let scope = scope_stack.resolve_scope(source_lookup); + u.gate = scope.name.to_string(); + u.is_adjoint = scope.is_adjoint; + let scope_location = scope + .location + .map(|loc| source_lookup.resolve_package_offset(&loc)); if let Some(metadata) = &mut u.metadata { metadata.scope_location = scope_location; } else { @@ -1073,7 +1167,7 @@ impl OperationOrGroup { u.children = vec![ComponentColumn { components: children .into_iter() - .map(|o| o.into_operation(scope_resolver)) + .map(|o| o.into_operation(source_lookup)) .collect(), }]; self.op @@ -1113,7 +1207,7 @@ impl OperationListBuilder { } } - fn push_op(&mut self, op: OperationOrGroup, unfiltered_call_stack: Vec) { + fn push_op(&mut self, op: OperationOrGroup, unfiltered_call_stack: LogicalStack) { 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; @@ -1123,7 +1217,7 @@ impl OperationListBuilder { let op_call_stack = if self.group_by_scope || self.source_locations { retain_user_frames(&self.user_package_ids, unfiltered_call_stack) } else { - vec![] + LogicalStack::default() }; add_scoped_op( @@ -1151,7 +1245,7 @@ impl OperationListBuilder { is_adjoint: bool, inputs: &GateInputs, args: Vec, - call_stack: Vec, + call_stack: LogicalStack, ) { let targets = inputs .targets @@ -1169,13 +1263,7 @@ impl OperationListBuilder { ); } - fn m( - &mut self, - wire_map: &WireMap, - qubit: usize, - result: usize, - call_stack: Vec, - ) { + fn m(&mut self, wire_map: &WireMap, qubit: usize, result: usize, call_stack: LogicalStack) { let qubit = wire_map.qubit_wire(qubit); let result = wire_map.result_wire(result); self.push_op( @@ -1189,7 +1277,7 @@ impl OperationListBuilder { wire_map: &WireMap, qubit: usize, result: usize, - call_stack: Vec, + call_stack: LogicalStack, ) { let qubit = wire_map.qubit_wire(qubit); let result = wire_map.result_wire(result); @@ -1200,7 +1288,7 @@ impl OperationListBuilder { self.push_op(OperationOrGroup::new_ket(qubit), call_stack); } - fn reset(&mut self, wire_map: &WireMap, qubit: usize, call_stack: Vec) { + fn reset(&mut self, wire_map: &WireMap, qubit: usize, call_stack: LogicalStack) { let qubit = wire_map.qubit_wire(qubit); self.push_op(OperationOrGroup::new_ket(qubit), call_stack); } @@ -1211,103 +1299,41 @@ struct GateInputs<'a> { 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, - }, +pub struct LexicalScope { + /// The start offset of the scope, used for navigation. + location: Option, + /// A display name for the scope. + name: Rc, + /// Whether the scope represents an adjoint operation, + /// used for display purposes. + is_adjoint: bool, } -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, mut op: OperationOrGroup, - op_call_stack: &[LocationMetadata], + op_call_stack: &LogicalStack, group_by_scope: bool, set_source_location: bool, ) { - if set_source_location && let Some(called_at) = op_call_stack.last() { - op.set_location(called_at.source_location()); + if set_source_location && let Some(called_at) = op_call_stack.0.last() { + op.location = Some(*called_at.location()); } - let op_call_stack = if group_by_scope { op_call_stack } else { &[] }; + let default = LogicalStack::default(); + let op_call_stack = if group_by_scope { + op_call_stack + } else { + &default + }; 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 !relative_stack.0.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() @@ -1334,16 +1360,20 @@ fn add_scoped_op( } } - let op_scope_stack = scope_stack(op_call_stack); + let op_scope_stack = scope_stack(&op_call_stack.0); 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(); + let parent = LogicalStack( + op_call_stack + .0 + .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, @@ -1363,74 +1393,46 @@ fn add_scoped_op( fn retain_user_frames( user_package_ids: &[PackageId], - mut location_stack: Vec, -) -> Vec { - location_stack.retain(|location| { + mut location_stack: LogicalStack, +) -> LogicalStack { + location_stack.0.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 - } + LogicalStack(location_stack.0) } /// Represents a scope in the call stack, tracking the caller chain and current scope identifier. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] struct ScopeStack { - caller: Vec, - scope: ScopeId, + caller: LogicalStack, + scope: Scope, } impl ScopeStack { - fn caller(&self) -> &[LocationMetadata] { - &self.caller + fn caller(&self) -> &[LogicalStackEntry] { + &self.caller.0 } - fn current_lexical_scope(&self) -> ScopeId { + fn current_lexical_scope(&self) -> Scope { 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() + self.caller.0.is_empty() && self.scope == Scope::default() } fn top() -> Self { ScopeStack { - caller: Vec::new(), - scope: ScopeId::default(), + caller: LogicalStack::default(), + scope: Scope::default(), } } - fn resolve_scope(&self, scope_resolver: &impl SourceLookup) -> LexicalScope { - if self.is_top() { - LexicalScope::top() - } else { - scope_resolver.resolve_scope(self.scope) - } + fn resolve_scope(&self, source_lookup: &impl SourceLookup) -> LexicalScope { + source_lookup.resolve_scope(self.scope) } } @@ -1445,28 +1447,230 @@ impl ScopeStack { /// 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], + full_call_stack: &LogicalStack, prefix_scope_stack: &ScopeStack, -) -> Option> { +) -> Option { if prefix_scope_stack.is_top() { - return Some(full_call_stack.to_vec()); + return Some(full_call_stack.clone()); } - if full_call_stack.len() > prefix_scope_stack.caller().len() - && let Some(rest) = full_call_stack.strip_prefix(prefix_scope_stack.caller()) + if full_call_stack.0.len() > prefix_scope_stack.caller().len() + && let Some(rest) = full_call_stack.0.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()); + return Some(LogicalStack(rest.to_vec())); } None } -fn scope_stack(instruction_stack: &[LocationMetadata]) -> ScopeStack { +fn scope_stack(instruction_stack: &[LogicalStackEntry]) -> ScopeStack { instruction_stack .split_last() .map_or(ScopeStack::top(), |(youngest, prefix)| ScopeStack { - caller: prefix.to_vec(), + caller: LogicalStack(prefix.to_vec()), scope: youngest.lexical_scope(), }) } + +#[derive(Clone, Default, PartialEq)] +/// A "logical" stack trace. This is a processed version of a raw stack trace +/// captured from the evaluator. +/// This stack trace doesn't only contain calls to callables, but also entries into scopes +/// that are deemed to be interesting such as loops and lexical blocks. +pub struct LogicalStack(pub Vec); + +impl LogicalStack { + #[must_use] + pub fn from_evaluator_trace(trace: &StackTrace) -> Self { + let call_stack = trace + .call_stack() + .iter() + .enumerate() + .flat_map(|(frame_idx, frame)| { + let mut logical_stack = vec![LogicalStackEntry::new_call_site( + PackageOffset { + package_id: frame.id.package, + offset: frame.span.lo, + }, + Scope::Callable(frame.id, frame.functor), + )]; + + // Insert any loop frames + let mut iteration_count: usize = 0; + for block_scope in trace.scope_stack() { + if block_scope.frame_id == frame_idx + 1 { + let last = logical_stack.last_mut().expect("there should be a frame"); + + let current_loop_scope_and_call_to_current = + if let Some(loop_scope) = &block_scope.loop_scope { + iteration_count = loop_scope.iteration_count; + // This is the containing scope for a loop + Some(( + Scope::Loop(frame.id.package, loop_scope.loop_expr), + LogicalStackEntryLocation::Loop( + frame.id.package, + loop_scope.loop_expr, + ), + )) + } else if let Scope::Loop(package_id, loop_expr) = &last.scope { + // This is a block immediately inside a loop, so this is an iteration scope + Some(( + Scope::LoopIteration(*package_id, *loop_expr, iteration_count), + LogicalStackEntryLocation::LoopIteration( + *package_id, + *loop_expr, + iteration_count, + ), + )) + } else { + None + }; + + if let Some((current_scope, call_to_current)) = + current_loop_scope_and_call_to_current + { + let last_call_site = last.location; + last.location = call_to_current; + + logical_stack + .push(LogicalStackEntry::new(last_call_site, current_scope)); + } + } + } + logical_stack + }) + .collect::>(); + + LogicalStack(call_stack) + } +} + +/// An entry in a logical stack trace. +#[derive(Clone, Debug, PartialEq)] +pub struct LogicalStackEntry { + /// Location of the "call" into the next entry. + /// The location type should correspond to the next entry's scope, e.g. a `LogicalStackEntryLocation::Call` + /// would be followed by a `Scope::Callable` in the stack trace. + /// Used as a discriminator when grouping. Within a scope, each distinct call/loop should have a unique location. + location: LogicalStackEntryLocation, + /// The lexical scope of this stack trace entry. + /// Instructions that share a scope will be grouped together in the circuit diagram. + scope: Scope, +} + +impl LogicalStackEntry { + #[must_use] + pub fn lexical_scope(&self) -> Scope { + self.scope + } + + #[must_use] + pub fn location(&self) -> &LogicalStackEntryLocation { + &self.location + } + + #[must_use] + pub fn package_id(&self) -> PackageId { + match self.scope { + Scope::Top => panic!("top scope has no package"), + Scope::Callable(store_item_id, _) => store_item_id.package, + Scope::LoopIteration(package_id, _, _) | Scope::Loop(package_id, _) => package_id, + } + } + + fn new_call_site(package_offset: PackageOffset, scope: Scope) -> Self { + Self { + location: LogicalStackEntryLocation::Call(package_offset), + scope, + } + } + + fn new(location: LogicalStackEntryLocation, scope: Scope) -> Self { + Self { location, scope } + } +} + +#[derive(Clone, Debug, Copy, PartialEq)] +/// In a stack trace, represents the location of each entry. +pub enum LogicalStackEntryLocation { + /// A call to a callable at the given package offset. + Call(PackageOffset), + /// An entry into a loop. The `ExprId` identifies the loop expression. + Loop(PackageId, ExprId), + /// An iteration of a loop. The `usize` is the iteration count + /// and is used to discriminate different iterations. The `ExprId` identifies + /// the loop expression. + LoopIteration(PackageId, ExprId, usize), +} + +#[derive(Clone, Debug, PartialEq, Copy, Default)] +pub enum Scope { + #[default] + /// The top-level scope. + Top, + /// A callable. + Callable(StoreItemId, FunctorApp), + /// A loop. The `ExprId` identifies the loop expression. + Loop(PackageId, ExprId), + /// A loop body. The `ExprId` identifies the loop expression. + /// The `usize` is the iteration count. + LoopIteration(PackageId, ExprId, usize), +} + +impl Display for Scope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Scope::Top => write!(f, "top"), + Scope::Callable(i, _) => { + write!(f, "callable{}-{}", i.package, i.item) + } + Scope::LoopIteration(_, _, i) => { + write!(f, "loop-iter({i})") + } + Scope::Loop(..) => { + write!(f, "loop-container") + } + } + } +} + +#[derive(Clone, Debug, Copy, PartialEq, Eq)] +pub struct PackageOffset { + pub package_id: PackageId, + pub offset: u32, +} + +#[cfg(test)] +struct LogicalStackWithSourceLookup<'a, S> { + trace: LogicalStack, + source_lookup: &'a S, +} + +#[cfg(test)] +impl Display for LogicalStackWithSourceLookup<'_, S> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (i, frame) in self.trace.0.iter().enumerate() { + if i > 0 { + write!(f, " -> ")?; + } + + let scope = self.source_lookup.resolve_scope(frame.scope); + write!( + f, + "{}{}@", + scope.name, + if scope.is_adjoint { "†" } else { "" }, + )?; + let package_offset = self + .source_lookup + .resolve_logical_stack_entry_location(frame.location); + let l = self.source_lookup.resolve_package_offset(&package_offset); + write!(f, "{}:{}:{}", l.file, l.line, l.column)?; + if let LogicalStackEntryLocation::LoopIteration(_, _, iteration) = frame.location { + write!(f, "[{iteration}]")?; + } + } + Ok(()) + } +} diff --git a/source/compiler/qsc_circuit/src/builder/tests.rs b/source/compiler/qsc_circuit/src/builder/tests.rs index 153317bed3..4cd91e34a4 100644 --- a/source/compiler/qsc_circuit/src/builder/tests.rs +++ b/source/compiler/qsc_circuit/src/builder/tests.rs @@ -4,6 +4,7 @@ #![allow(clippy::unicode_not_nfc)] mod group_scopes; +mod logical_stack_trace; mod prune_classical_qubits; use std::vec; @@ -11,6 +12,8 @@ use std::vec; use super::*; use expect_test::expect; use qsc_data_structures::{functors::FunctorApp, span::Span}; +use qsc_eval::debug::Frame; +use qsc_fir::fir::StoreItemId; use rustc_hash::FxHashMap; #[derive(Default)] @@ -19,8 +22,8 @@ struct FakeCompilation { } impl SourceLookup for FakeCompilation { - fn resolve_location(&self, package_offset: &PackageOffset) -> ResolvedSourceLocation { - ResolvedSourceLocation { + fn resolve_package_offset(&self, package_offset: &PackageOffset) -> SourceLocation { + SourceLocation { file: match usize::from(package_offset.package_id) { Self::USER_PACKAGE_ID => "user_code.qs".to_string(), Self::LIBRARY_PACKAGE_ID => "library_code.qs".to_string(), @@ -31,20 +34,35 @@ impl SourceLookup for FakeCompilation { } } - 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, + fn resolve_scope(&self, scope: Scope) -> LexicalScope { + match scope { + Scope::Callable(store_item_id, functor_app) => { + let name = self + .scopes + .id_to_name + .get(&store_item_id) + .expect("unknown scope id") + .clone(); + LexicalScope { + name, + location: Some(PackageOffset { + package_id: store_item_id.package, + offset: 0, + }), + is_adjoint: functor_app.adjoint, + } + } + s => panic!("unexpected scope id {s:?}"), + } + } + + fn resolve_logical_stack_entry_location( + &self, + location: LogicalStackEntryLocation, + ) -> PackageOffset { + match location { + LogicalStackEntryLocation::Call(package_offset) => package_offset, + _ => todo!("location type not implemented for fake compilation"), } } } @@ -78,18 +96,21 @@ impl FakeCompilation { Self::frame(scope_id, offset, true) } - fn frame(scope_item_id: ScopeId, offset: u32, is_adjoint: bool) -> Frame { - Frame { - span: Span { - lo: offset, - hi: offset + 1, - }, - id: scope_item_id.0, - caller: PackageId::CORE, // unused in tests - functor: FunctorApp { - adjoint: is_adjoint, - controlled: 0, + fn frame(scope_item_id: Scope, offset: u32, is_adjoint: bool) -> Frame { + match scope_item_id { + Scope::Callable(store_item_id, _) => Frame { + span: Span { + lo: offset, + hi: offset + 1, + }, + id: store_item_id, + caller: PackageId::CORE, // unused in tests + functor: FunctorApp { + adjoint: is_adjoint, + controlled: 0, + }, }, + _ => panic!("unexpected scope id {scope_item_id:?}"), } } } @@ -101,7 +122,7 @@ struct Scopes { } impl Scopes { - fn get_or_create_scope(&mut self, package_id: usize, name: &str, is_adjoint: bool) -> ScopeId { + fn get_or_create_scope(&mut self, package_id: usize, name: &str, is_adjoint: bool) -> Scope { let name: Rc = name.into(); let item_id = if let Some(item_id) = self.name_to_id.get(&name) { *item_id @@ -114,7 +135,7 @@ impl Scopes { self.name_to_id.insert(name, item_id); item_id }; - ScopeId( + Scope::Callable( item_id, FunctorApp { adjoint: is_adjoint, @@ -136,11 +157,11 @@ fn exceed_max_operations() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); - builder.gate(&[], "X", false, &[0], &[], None); - builder.gate(&[], "X", false, &[0], &[], None); - builder.gate(&[], "X", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[0], &[], None); let circuit = builder.finish(&FakeCompilation::default()); @@ -165,10 +186,10 @@ fn source_locations_enabled() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); builder.gate( - &[c.user_code_frame("Main", 10)], + &stack_trace(vec![c.user_code_frame("Main", 10)]), "X", false, &[0], @@ -203,10 +224,10 @@ fn source_locations_disabled() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); builder.gate( - &[c.user_code_frame("Main", 10)], + &stack_trace(vec![c.user_code_frame("Main", 10)]), "X", false, &[0], @@ -235,10 +256,13 @@ fn source_locations_multiple_user_frames() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); builder.gate( - &[c.user_code_frame("Main", 10), c.user_code_frame("Main", 20)], + &stack_trace(vec![ + c.user_code_frame("Main", 10), + c.user_code_frame("Main", 20), + ]), "X", false, &[0], @@ -274,9 +298,9 @@ fn source_locations_library_frames_excluded() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); builder.gate( - &[c.user_code_frame("Main", 10), c.library_frame(20)], + &stack_trace(vec![c.user_code_frame("Main", 10), c.library_frame(20)]), "X", false, &[0], @@ -307,10 +331,10 @@ fn source_locations_only_library_frames() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); builder.gate( - &[c.library_frame(20), c.library_frame(30)], + &stack_trace(vec![c.library_frame(20), c.library_frame(30)]), "X", false, &[0], @@ -340,9 +364,9 @@ fn source_locations_enabled_no_stack() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); - builder.gate(&[], "X", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[0], &[], None); let circuit = builder.finish(&c); @@ -366,9 +390,9 @@ fn qubit_source_locations_via_stack() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[c.user_code_frame("Main", 10)], 0); + builder.qubit_allocate(&stack_trace(vec![c.user_code_frame("Main", 10)]), 0); - builder.gate(&[], "X", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[0], &[], None); let circuit = builder.finish(&c); @@ -398,10 +422,10 @@ fn qubit_labels_for_preallocated_qubits() { )), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); builder.gate( - &[c.user_code_frame("Main", 20)], + &stack_trace(vec![c.user_code_frame("Main", 20)]), "X", false, &[0], @@ -431,11 +455,23 @@ fn measurement_target_propagated_to_group() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); - builder.gate(&[c.user_code_frame("Main", 1)], "H", false, &[0], &[], None); + builder.gate( + &stack_trace(vec![c.user_code_frame("Main", 1)]), + "H", + false, + &[0], + &[], + None, + ); - builder.measure(&[c.user_code_frame("Main", 2)], "M", 0, &0.into()); + builder.measure( + &stack_trace(vec![c.user_code_frame("Main", 2)]), + "M", + 0, + &0.into(), + ); let circuit = builder.finish(&c); @@ -513,10 +549,13 @@ fn source_locations_for_groups() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); + builder.qubit_allocate(&StackTrace::default(), 0); builder.gate( - &[c.user_code_frame("Main", 10), c.user_code_frame("Foo", 10)], + &stack_trace(vec![ + c.user_code_frame("Main", 10), + c.user_code_frame("Foo", 10), + ]), "X", false, &[0], @@ -531,3 +570,7 @@ fn source_locations_for_groups() { "#]] .assert_eq(&circuit.display_with_groups().to_string()); } + +pub(crate) fn stack_trace(frames: Vec) -> StackTrace { + StackTrace::new(frames, vec![]) +} diff --git a/source/compiler/qsc_circuit/src/builder/tests/group_scopes.rs b/source/compiler/qsc_circuit/src/builder/tests/group_scopes.rs index 37a9c718b1..a9745f9d35 100644 --- a/source/compiler/qsc_circuit/src/builder/tests/group_scopes.rs +++ b/source/compiler/qsc_circuit/src/builder/tests/group_scopes.rs @@ -2,9 +2,9 @@ // Licensed under the MIT License. use super::FakeCompilation; -use crate::{CircuitTracer, TracerConfig}; +use crate::{CircuitTracer, TracerConfig, builder::tests::stack_trace}; use expect_test::{Expect, expect}; -use qsc_eval::{backend::Tracer, debug::Frame}; +use qsc_eval::{StackTrace, backend::Tracer, debug::Frame}; fn check_groups(c: &FakeCompilation, instructions: &[(Vec, &str)], expect: &Expect) { let mut tracer = CircuitTracer::new( @@ -20,14 +20,12 @@ fn check_groups(c: &FakeCompilation, instructions: &[(Vec, &str)], expect let qubit_id = 0; // Allocate qubit 0 - tracer.qubit_allocate(&[], qubit_id); + tracer.qubit_allocate(&StackTrace::default(), 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); + for (stack, instruction_name) in instructions { + let stack = stack_trace(stack.clone()); + tracer.gate(&stack, instruction_name, false, &[qubit_id], &[], None); } let circuit = tracer.finish(c); diff --git a/source/compiler/qsc_circuit/src/builder/tests/logical_stack_trace.rs b/source/compiler/qsc_circuit/src/builder/tests/logical_stack_trace.rs new file mode 100644 index 0000000000..01552a2797 --- /dev/null +++ b/source/compiler/qsc_circuit/src/builder/tests/logical_stack_trace.rs @@ -0,0 +1,611 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fmt::Write; + +use expect_test::{Expect, expect}; +use indoc::indoc; +use qsc_data_structures::{ + language_features::LanguageFeatures, source::SourceMap, target::TargetCapabilityFlags, +}; +use qsc_eval::{ + Env, ErrorBehavior, State, StepAction, + backend::{SparseSim, Tracer, TracingBackend}, + output::GenericReceiver, + val::{self}, +}; +use qsc_fir::fir::{self, ExecGraphConfig}; +use qsc_frontend::compile::{self, PackageStore, compile}; +use qsc_lowerer::map_hir_package_to_fir; +use qsc_passes::{PackageType, run_core_passes, run_default_passes}; + +use crate::builder::{LogicalStack, LogicalStackWithSourceLookup}; + +struct TestTracer<'a> { + trace: String, + is_stack_tracing_enabled: bool, + source_lookup: &'a (&'a compile::PackageStore, &'a fir::PackageStore), +} + +impl Tracer for TestTracer<'_> { + fn qubit_allocate(&mut self, stack: &qsc_eval::StackTrace, q: usize) { + self.write_stack(stack); + let _ = writeln!(self.trace, "qubit_allocate(q_{q})"); + } + + fn qubit_release(&mut self, stack: &qsc_eval::StackTrace, q: usize) { + self.write_stack(stack); + let _ = writeln!(self.trace, "qubit_release(q_{q})"); + } + + fn qubit_swap_id(&mut self, stack: &qsc_eval::StackTrace, q0: usize, q1: usize) { + self.write_stack(stack); + let _ = writeln!(self.trace, "qubit_swap_id(q_{q0} q_{q1})"); + } + + fn gate( + &mut self, + stack: &qsc_eval::StackTrace, + name: &str, + is_adjoint: bool, + targets: &[usize], + controls: &[usize], + theta: Option, + ) { + self.write_stack(stack); + let _ = writeln!( + self.trace, + "gate({}{}{}, targets=({}), controls=({}))", + name, + if is_adjoint { "†" } else { "" }, + theta.map(|t| format!("({t:.4})")).unwrap_or_default(), + targets + .iter() + .map(|q| format!("q_{q}")) + .collect::>() + .join(", "), + controls + .iter() + .map(|q| format!("q_{q}")) + .collect::>() + .join(", "), + ); + } + + fn measure(&mut self, stack: &qsc_eval::StackTrace, name: &str, q: usize, r: &val::Result) { + self.write_stack(stack); + let _ = writeln!(self.trace, "measure({name}, q_{q}, {r:?})"); + } + + fn reset(&mut self, stack: &qsc_eval::StackTrace, q: usize) { + self.write_stack(stack); + let _ = writeln!(self.trace, "reset(q_{q})"); + } + + fn custom_intrinsic(&mut self, stack: &qsc_eval::StackTrace, name: &str, arg: val::Value) { + self.write_stack(stack); + let _ = writeln!(self.trace, "intrinsic({name}, {arg})"); + } + + fn is_stack_tracing_enabled(&self) -> bool { + self.is_stack_tracing_enabled + } +} + +impl TestTracer<'_> { + fn write_stack(&mut self, stack: &qsc_eval::StackTrace) { + let trace = LogicalStack::from_evaluator_trace(stack); + let display = LogicalStackWithSourceLookup { + trace, + source_lookup: self.source_lookup, + }; + if display.trace.0.is_empty() { + let _ = write!(self.trace, "[no stack] "); + } else { + let _ = write!(self.trace, "{display} -> "); + } + } +} + +fn check_trace(file: &str, expr: &str, exec_graph_config: ExecGraphConfig, expect: &Expect) { + let mut fir_lowerer = qsc_lowerer::Lowerer::new(); + let mut core = compile::core(); + run_core_passes(&mut core); + let fir_store = fir::PackageStore::new(); + let core_fir = fir_lowerer.lower_package(&core.package, &fir_store); + let mut store = PackageStore::new(core); + + let mut std = compile::std(&store, TargetCapabilityFlags::all()); + assert!(std.errors.is_empty()); + assert!(run_default_passes(store.core(), &mut std, PackageType::Lib).is_empty()); + let std_fir = fir_lowerer.lower_package(&std.package, &fir_store); + let std_id = store.insert(std); + + let sources = SourceMap::new([("A.qs".into(), file.into())], Some(expr.into())); + let mut unit = compile( + &store, + &[(std_id, None)], + sources, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + assert!(unit.errors.is_empty(), "{:?}", unit.errors); + let pass_errors = run_default_passes(store.core(), &mut unit, PackageType::Lib); + assert!(pass_errors.is_empty(), "{pass_errors:?}"); + let unit_fir = fir_lowerer.lower_package(&unit.package, &fir_store); + let entry = unit_fir.entry_exec_graph.clone(); + let id = store.insert(unit); + + let mut fir_store = fir::PackageStore::new(); + fir_store.insert( + map_hir_package_to_fir(qsc_hir::hir::PackageId::CORE), + core_fir, + ); + fir_store.insert(map_hir_package_to_fir(std_id), std_fir); + fir_store.insert(map_hir_package_to_fir(id), unit_fir); + + let mut out = Vec::new(); + let mut state = State::new( + map_hir_package_to_fir(id), + entry, + exec_graph_config, + None, + ErrorBehavior::FailOnError, + ); + + let mut tracer = TestTracer { + trace: String::new(), + is_stack_tracing_enabled: true, + source_lookup: &(&store, &fir_store), + }; + let mut tracing_backend = TracingBackend::::no_backend(&mut tracer); + let _ = state.eval( + &fir_store, + &mut Env::default(), + &mut tracing_backend, + &mut GenericReceiver::new(&mut out), + &[], + StepAction::Continue, + ); + expect.assert_eq(&tracer.trace); +} + +#[test] +fn no_sim_calls() { + check_trace( + indoc! {r#" + operation Main() : Unit { + for i in 0..2 { + Message("Hello"); + } + } + "#}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![""], + ); +} + +#[test] +fn gate() { + check_trace( + indoc! {" + operation Main() : Unit { + use q = Qubit(); + X(q); + X(q); + } + "}, + "A.Main()", + ExecGraphConfig::NoDebug, + &expect![[r#" + Main@A.qs:1:4 -> qubit_allocate(q_0) + Main@A.qs:2:4 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_0), controls=()) + Main@A.qs:3:4 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_0), controls=()) + Main@A.qs:1:4 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn multi_qubit_alloc_debug() { + check_trace( + indoc! {" + operation Main() : Unit { + use q = Qubit[3]; + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:1:4 -> AllocateQubitArray@qsharp-library-source:core/qir.qs:17:8 -> loop: 0..size - 1@qsharp-library-source:core/qir.qs:17:29[1] -> (1)@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_0) + Main@A.qs:1:4 -> AllocateQubitArray@qsharp-library-source:core/qir.qs:17:8 -> loop: 0..size - 1@qsharp-library-source:core/qir.qs:17:29[2] -> (2)@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_1) + Main@A.qs:1:4 -> AllocateQubitArray@qsharp-library-source:core/qir.qs:17:8 -> loop: 0..size - 1@qsharp-library-source:core/qir.qs:17:29[3] -> (3)@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_2) + Main@A.qs:1:4 -> ReleaseQubitArray@qsharp-library-source:core/qir.qs:24:8 -> loop: qs@qsharp-library-source:core/qir.qs:24:20[1] -> (1)@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_0) + Main@A.qs:1:4 -> ReleaseQubitArray@qsharp-library-source:core/qir.qs:24:8 -> loop: qs@qsharp-library-source:core/qir.qs:24:20[2] -> (2)@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_1) + Main@A.qs:1:4 -> ReleaseQubitArray@qsharp-library-source:core/qir.qs:24:8 -> loop: qs@qsharp-library-source:core/qir.qs:24:20[3] -> (3)@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_2) + "#]], + ); +} + +#[test] +fn multi_qubit_alloc_no_debug() { + check_trace( + indoc! {" + operation Main() : Unit { + use q = Qubit[3]; + } + "}, + "A.Main()", + ExecGraphConfig::NoDebug, + &expect![[r#" + Main@A.qs:1:4 -> AllocateQubitArray@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_0) + Main@A.qs:1:4 -> AllocateQubitArray@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_1) + Main@A.qs:1:4 -> AllocateQubitArray@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_2) + Main@A.qs:1:4 -> ReleaseQubitArray@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_0) + Main@A.qs:1:4 -> ReleaseQubitArray@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_1) + Main@A.qs:1:4 -> ReleaseQubitArray@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_2) + "#]], + ); +} + +#[test] +fn qubit_alloc_in_loop() { + check_trace( + indoc! {" + operation Main() : Unit { + for i in 1..2 { + use q = Qubit(); + H(q); + } + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:1:4 -> loop: 1..2@A.qs:1:18[1] -> (1)@A.qs:2:8 -> qubit_allocate(q_0) + Main@A.qs:1:4 -> loop: 1..2@A.qs:1:18[1] -> (1)@A.qs:3:8 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:1:4 -> loop: 1..2@A.qs:1:18[1] -> (1)@A.qs:2:8 -> qubit_release(q_0) + Main@A.qs:1:4 -> loop: 1..2@A.qs:1:18[2] -> (2)@A.qs:2:8 -> qubit_allocate(q_0) + Main@A.qs:1:4 -> loop: 1..2@A.qs:1:18[2] -> (2)@A.qs:3:8 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:1:4 -> loop: 1..2@A.qs:1:18[2] -> (2)@A.qs:2:8 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn nested_callables() { + check_trace( + indoc! {" + operation Main() : Unit { + use q = Qubit(); + Foo(q); + MResetZ(q); + } + + operation Foo(q: Qubit) : Unit { + H(q); + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:1:4 -> qubit_allocate(q_0) + Main@A.qs:2:4 -> Foo@A.qs:7:4 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:3:4 -> MResetZ@qsharp-library-source:Std/Measurement.qs:135:4 -> measure(MResetZ, q_0, Id(0)) + Main@A.qs:1:4 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn for_loop_debug() { + check_trace( + indoc! {" + operation Main() : Unit { + use q = Qubit(); + for i in 0..2 { + H(q); + } + MResetZ(q); + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:1:4 -> qubit_allocate(q_0) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[1] -> (1)@A.qs:3:8 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[2] -> (2)@A.qs:3:8 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[3] -> (3)@A.qs:3:8 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:5:4 -> MResetZ@qsharp-library-source:Std/Measurement.qs:135:4 -> measure(MResetZ, q_0, Id(0)) + Main@A.qs:1:4 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn for_loop_no_debug() { + check_trace( + indoc! {" + operation Main() : Unit { + use q = Qubit(); + for i in 0..2 { + H(q); + } + MResetZ(q); + } + "}, + "A.Main()", + ExecGraphConfig::NoDebug, + &expect![[r#" + Main@A.qs:1:4 -> qubit_allocate(q_0) + Main@A.qs:3:8 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:3:8 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:3:8 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:5:4 -> MResetZ@qsharp-library-source:Std/Measurement.qs:135:4 -> measure(MResetZ, q_0, Id(0)) + Main@A.qs:1:4 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn nested_callables_and_loop() { + check_trace( + indoc! {" + operation Main() : Unit { + use q = Qubit(); + for i in 0..2 { + Foo(q); + } + MResetZ(q); + } + + operation Foo(q: Qubit) : Unit { + H(q); + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:1:4 -> qubit_allocate(q_0) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[1] -> (1)@A.qs:3:8 -> Foo@A.qs:9:4 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[2] -> (2)@A.qs:3:8 -> Foo@A.qs:9:4 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[3] -> (3)@A.qs:3:8 -> Foo@A.qs:9:4 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:5:4 -> MResetZ@qsharp-library-source:Std/Measurement.qs:135:4 -> measure(MResetZ, q_0, Id(0)) + Main@A.qs:1:4 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn repeat_until_loop() { + check_trace( + indoc! {" + operation Main() : Unit { + use q = Qubit(); + for i in 0..2 { + Foo(q); + } + MResetZ(q); + } + + operation Foo(q: Qubit) : Unit { + H(q); + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:1:4 -> qubit_allocate(q_0) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[1] -> (1)@A.qs:3:8 -> Foo@A.qs:9:4 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[2] -> (2)@A.qs:3:8 -> Foo@A.qs:9:4 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[3] -> (3)@A.qs:3:8 -> Foo@A.qs:9:4 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + Main@A.qs:5:4 -> MResetZ@qsharp-library-source:Std/Measurement.qs:135:4 -> measure(MResetZ, q_0, Id(0)) + Main@A.qs:1:4 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn while_loop() { + check_trace( + indoc! {" + operation Main() : Unit { + use q = Qubit(); + mutable i = 0; + while (i < 2) { + Foo(q); + set i += 1; + } + } + + operation Foo(q: Qubit) : Unit { + Y(q); + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:1:4 -> qubit_allocate(q_0) + Main@A.qs:3:4 -> loop: i < 2@A.qs:3:18[1] -> (1)@A.qs:4:8 -> Foo@A.qs:10:4 -> Y@qsharp-library-source:Std/Intrinsic.qs:1082:8 -> gate(Y, targets=(q_0), controls=()) + Main@A.qs:3:4 -> loop: i < 2@A.qs:3:18[2] -> (2)@A.qs:4:8 -> Foo@A.qs:10:4 -> Y@qsharp-library-source:Std/Intrinsic.qs:1082:8 -> gate(Y, targets=(q_0), controls=()) + Main@A.qs:1:4 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn nested_for_loop() { + check_trace( + indoc! {" + operation Main() : Unit { + use qs = Qubit[2]; + for j in 0..2 { + for i in 0..1 { + Foo(qs[i]); + } + } + } + + operation Foo(q: Qubit) : Unit { + X(q); + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:1:4 -> AllocateQubitArray@qsharp-library-source:core/qir.qs:17:8 -> loop: 0..size - 1@qsharp-library-source:core/qir.qs:17:29[1] -> (1)@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_0) + Main@A.qs:1:4 -> AllocateQubitArray@qsharp-library-source:core/qir.qs:17:8 -> loop: 0..size - 1@qsharp-library-source:core/qir.qs:17:29[2] -> (2)@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_1) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[1] -> (1)@A.qs:3:8 -> loop: 0..1@A.qs:3:22[1] -> (1)@A.qs:4:12 -> Foo@A.qs:10:4 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_0), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[1] -> (1)@A.qs:3:8 -> loop: 0..1@A.qs:3:22[2] -> (2)@A.qs:4:12 -> Foo@A.qs:10:4 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_1), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[2] -> (2)@A.qs:3:8 -> loop: 0..1@A.qs:3:22[1] -> (1)@A.qs:4:12 -> Foo@A.qs:10:4 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_0), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[2] -> (2)@A.qs:3:8 -> loop: 0..1@A.qs:3:22[2] -> (2)@A.qs:4:12 -> Foo@A.qs:10:4 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_1), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[3] -> (3)@A.qs:3:8 -> loop: 0..1@A.qs:3:22[1] -> (1)@A.qs:4:12 -> Foo@A.qs:10:4 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_0), controls=()) + Main@A.qs:2:4 -> loop: 0..2@A.qs:2:18[3] -> (3)@A.qs:3:8 -> loop: 0..1@A.qs:3:22[2] -> (2)@A.qs:4:12 -> Foo@A.qs:10:4 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_1), controls=()) + Main@A.qs:1:4 -> ReleaseQubitArray@qsharp-library-source:core/qir.qs:24:8 -> loop: qs@qsharp-library-source:core/qir.qs:24:20[1] -> (1)@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_0) + Main@A.qs:1:4 -> ReleaseQubitArray@qsharp-library-source:core/qir.qs:24:8 -> loop: qs@qsharp-library-source:core/qir.qs:24:20[2] -> (2)@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_1) + "#]], + ); +} + +#[test] +fn qubit_reuse() { + check_trace( + indoc! {" + operation Main() : Unit { + { + use q1 = Qubit(); + X(q1); + MResetZ(q1); + } + { + use q2 = Qubit(); + Y(q2); + MResetZ(q2); + } + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:2:8 -> qubit_allocate(q_0) + Main@A.qs:3:8 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_0), controls=()) + Main@A.qs:4:8 -> MResetZ@qsharp-library-source:Std/Measurement.qs:135:4 -> measure(MResetZ, q_0, Id(0)) + Main@A.qs:2:8 -> qubit_release(q_0) + Main@A.qs:7:8 -> qubit_allocate(q_0) + Main@A.qs:8:8 -> Y@qsharp-library-source:Std/Intrinsic.qs:1082:8 -> gate(Y, targets=(q_0), controls=()) + Main@A.qs:9:8 -> MResetZ@qsharp-library-source:Std/Measurement.qs:135:4 -> measure(MResetZ, q_0, Id(1)) + Main@A.qs:7:8 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn custom_intrinsic() { + check_trace( + indoc! {" + operation foo(n: Int, q: Qubit): Unit { + body intrinsic; + } + + operation Main() : Unit { + use q = Qubit(); + X(q); + foo(4, q); + } + "}, + "A.Main()", + ExecGraphConfig::Debug, + &expect![[r#" + Main@A.qs:5:4 -> qubit_allocate(q_0) + Main@A.qs:6:4 -> X@qsharp-library-source:Std/Intrinsic.qs:1038:8 -> gate(X, targets=(q_0), controls=()) + Main@A.qs:7:4 -> intrinsic(foo, (4, Qubit0)) + Main@A.qs:5:4 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn entry_expr_allocates_qubits() { + // mimics how entry expressions are created when generating + // a circuit diagram for an operation. + check_trace( + indoc! {" + operation Test(q1: Qubit, q2: Qubit) : Result[] { + [M(q1), M(q2)] + } + "}, + indoc! {" + { + use qs = Qubit[2]; + (A.Test)(qs[0], qs[1]); + let r: Result[] = []; + r + }"}, + ExecGraphConfig::Debug, + &expect![[r#" + AllocateQubitArray@qsharp-library-source:core/qir.qs:17:8 -> loop: 0..size - 1@qsharp-library-source:core/qir.qs:17:29[1] -> (1)@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_0) + AllocateQubitArray@qsharp-library-source:core/qir.qs:17:8 -> loop: 0..size - 1@qsharp-library-source:core/qir.qs:17:29[2] -> (2)@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_1) + Test@A.qs:1:5 -> M@qsharp-library-source:Std/Intrinsic.qs:268:4 -> Measure@qsharp-library-source:Std/Intrinsic.qs:304:12 -> measure(M, q_0, Id(0)) + Test@A.qs:1:12 -> M@qsharp-library-source:Std/Intrinsic.qs:268:4 -> Measure@qsharp-library-source:Std/Intrinsic.qs:304:12 -> measure(M, q_1, Id(1)) + ReleaseQubitArray@qsharp-library-source:core/qir.qs:24:8 -> loop: qs@qsharp-library-source:core/qir.qs:24:20[1] -> (1)@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_0) + ReleaseQubitArray@qsharp-library-source:core/qir.qs:24:8 -> loop: qs@qsharp-library-source:core/qir.qs:24:20[2] -> (2)@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_1) + "#]], + ); +} + +#[test] +fn adjoint_operation_in_entry_expr() { + check_trace( + indoc! {" + operation Foo (q : Qubit) : Unit + is Adj + Ctl { + + body (...) { + X(q); + } + + adjoint (...) { + Y(q); + } + + controlled (cs, ...) { + } + } + "}, + indoc! {" + { + use qs = Qubit[1]; + (Adjoint A.Foo)(qs[0]); + let r: Result[] = []; + r + }"}, + ExecGraphConfig::Debug, + &expect![[r#" + AllocateQubitArray@qsharp-library-source:core/qir.qs:17:8 -> loop: 0..size - 1@qsharp-library-source:core/qir.qs:17:29[1] -> (1)@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_0) + Foo†@A.qs:8:8 -> Y@qsharp-library-source:Std/Intrinsic.qs:1082:8 -> gate(Y, targets=(q_0), controls=()) + ReleaseQubitArray@qsharp-library-source:core/qir.qs:24:8 -> loop: qs@qsharp-library-source:core/qir.qs:24:20[1] -> (1)@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_0) + "#]], + ); +} + +#[test] +fn lambda_in_entry_expr() { + check_trace( + indoc! {" + "}, + indoc! {" + { + use qs = Qubit[1]; + (q => H(q))(qs[0]); + let r: Result[] = []; + r + }"}, + ExecGraphConfig::Debug, + &expect![[r#" + AllocateQubitArray@qsharp-library-source:core/qir.qs:17:8 -> loop: 0..size - 1@qsharp-library-source:core/qir.qs:17:29[1] -> (1)@qsharp-library-source:core/qir.qs:18:23 -> qubit_allocate(q_0) + @:2:10 -> H@qsharp-library-source:Std/Intrinsic.qs:205:8 -> gate(H, targets=(q_0), controls=()) + ReleaseQubitArray@qsharp-library-source:core/qir.qs:24:8 -> loop: qs@qsharp-library-source:core/qir.qs:24:20[1] -> (1)@qsharp-library-source:core/qir.qs:25:12 -> qubit_release(q_0) + "#]], + ); +} diff --git a/source/compiler/qsc_circuit/src/builder/tests/prune_classical_qubits.rs b/source/compiler/qsc_circuit/src/builder/tests/prune_classical_qubits.rs index 20f93d03c1..269778fd7d 100644 --- a/source/compiler/qsc_circuit/src/builder/tests/prune_classical_qubits.rs +++ b/source/compiler/qsc_circuit/src/builder/tests/prune_classical_qubits.rs @@ -2,9 +2,12 @@ // Licensed under the MIT License. use expect_test::expect; -use qsc_eval::{backend::Tracer, val}; +use qsc_eval::{StackTrace, backend::Tracer, val}; -use crate::{CircuitTracer, TracerConfig, builder::tests::FakeCompilation}; +use crate::{ + CircuitTracer, TracerConfig, + builder::tests::{FakeCompilation, stack_trace}, +}; #[test] fn circuit_trimmed_stays_the_same() { @@ -18,13 +21,13 @@ fn circuit_trimmed_stays_the_same() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); - builder.gate(&[], "H", false, &[0], &[], None); - builder.gate(&[], "X", false, &[1], &[0], None); - builder.measure(&[], "MResetZ", 0, &val::Result::Id(0)); - builder.measure(&[], "MResetZ", 1, &val::Result::Id(1)); + builder.gate(&StackTrace::default(), "H", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[1], &[0], None); + builder.measure(&StackTrace::default(), "MResetZ", 0, &val::Result::Id(0)); + builder.measure(&StackTrace::default(), "MResetZ", 1, &val::Result::Id(1)); let circuit = builder.finish(&FakeCompilation::default()); @@ -49,14 +52,14 @@ fn circuit_trims_unused_qubit() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); - builder.qubit_allocate(&[], 2); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); + builder.qubit_allocate(&StackTrace::default(), 2); - builder.gate(&[], "H", false, &[0], &[], None); - builder.gate(&[], "X", false, &[2], &[0], None); - builder.measure(&[], "MResetZ", 0, &val::Result::Id(0)); - builder.measure(&[], "MResetZ", 2, &val::Result::Id(1)); + builder.gate(&StackTrace::default(), "H", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[2], &[0], None); + builder.measure(&StackTrace::default(), "MResetZ", 0, &val::Result::Id(0)); + builder.measure(&StackTrace::default(), "MResetZ", 2, &val::Result::Id(1)); let circuit = builder.finish(&FakeCompilation::default()); @@ -82,13 +85,20 @@ fn circuit_trims_unused_qubit_with_grouping() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[c.user_code_frame("Main", 1)], 0); - builder.qubit_allocate(&[c.user_code_frame("Main", 2)], 1); - builder.qubit_allocate(&[c.user_code_frame("Main", 3)], 2); + builder.qubit_allocate(&stack_trace(vec![c.user_code_frame("Main", 1)]), 0); + builder.qubit_allocate(&stack_trace(vec![c.user_code_frame("Main", 2)]), 1); + builder.qubit_allocate(&stack_trace(vec![c.user_code_frame("Main", 3)]), 2); - builder.gate(&[c.user_code_frame("Main", 4)], "H", false, &[0], &[], None); builder.gate( - &[c.user_code_frame("Main", 5)], + &stack_trace(vec![c.user_code_frame("Main", 4)]), + "H", + false, + &[0], + &[], + None, + ); + builder.gate( + &stack_trace(vec![c.user_code_frame("Main", 5)]), "X", false, &[2], @@ -96,13 +106,13 @@ fn circuit_trims_unused_qubit_with_grouping() { None, ); builder.measure( - &[c.user_code_frame("Main", 6)], + &stack_trace(vec![c.user_code_frame("Main", 6)]), "MResetZ", 0, &val::Result::Id(0), ); builder.measure( - &[c.user_code_frame("Main", 7)], + &stack_trace(vec![c.user_code_frame("Main", 7)]), "MResetZ", 2, &val::Result::Id(1), @@ -131,17 +141,16 @@ fn circuit_trims_classical_qubit() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); - builder.qubit_allocate(&[], 2); - - builder.gate(&[], "H", false, &[0], &[], None); - builder.gate(&[], "X", false, &[1], &[], None); - builder.gate(&[], "X", false, &[2], &[0], None); - builder.measure(&[], "MResetZ", 0, &val::Result::Id(0)); - builder.measure(&[], "MResetZ", 1, &val::Result::Id(1)); - builder.measure(&[], "MResetZ", 2, &val::Result::Id(2)); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); + builder.qubit_allocate(&StackTrace::default(), 2); + builder.gate(&StackTrace::default(), "H", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[1], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[2], &[0], None); + builder.measure(&StackTrace::default(), "MResetZ", 0, &val::Result::Id(0)); + builder.measure(&StackTrace::default(), "MResetZ", 1, &val::Result::Id(1)); + builder.measure(&StackTrace::default(), "MResetZ", 2, &val::Result::Id(2)); let circuit = builder.finish(&FakeCompilation::default()); expect![[r#" @@ -165,17 +174,16 @@ fn circuit_trims_classical_control_qubit() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); - builder.qubit_allocate(&[], 2); - - builder.gate(&[], "H", false, &[0], &[], None); - builder.gate(&[], "X", false, &[0], &[1], None); - builder.gate(&[], "X", false, &[2], &[0], None); - builder.measure(&[], "MResetZ", 0, &val::Result::Id(0)); - builder.measure(&[], "MResetZ", 1, &val::Result::Id(1)); - builder.measure(&[], "MResetZ", 2, &val::Result::Id(2)); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); + builder.qubit_allocate(&StackTrace::default(), 2); + builder.gate(&StackTrace::default(), "H", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[0], &[1], None); + builder.gate(&StackTrace::default(), "X", false, &[2], &[0], None); + builder.measure(&StackTrace::default(), "MResetZ", 0, &val::Result::Id(0)); + builder.measure(&StackTrace::default(), "MResetZ", 1, &val::Result::Id(1)); + builder.measure(&StackTrace::default(), "MResetZ", 2, &val::Result::Id(2)); let circuit = builder.finish(&FakeCompilation::default()); expect![[r#" @@ -199,17 +207,16 @@ fn circuit_trims_classical_qubit_when_2q_precedes_superposition() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); - builder.qubit_allocate(&[], 2); - - builder.gate(&[], "X", false, &[1], &[0], None); - builder.gate(&[], "H", false, &[0], &[], None); - builder.gate(&[], "X", false, &[2], &[0], None); - builder.measure(&[], "MResetZ", 0, &val::Result::Id(0)); - builder.measure(&[], "MResetZ", 1, &val::Result::Id(1)); - builder.measure(&[], "MResetZ", 2, &val::Result::Id(2)); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); + builder.qubit_allocate(&StackTrace::default(), 2); + builder.gate(&StackTrace::default(), "X", false, &[1], &[0], None); + builder.gate(&StackTrace::default(), "H", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[2], &[0], None); + builder.measure(&StackTrace::default(), "MResetZ", 0, &val::Result::Id(0)); + builder.measure(&StackTrace::default(), "MResetZ", 1, &val::Result::Id(1)); + builder.measure(&StackTrace::default(), "MResetZ", 2, &val::Result::Id(2)); let circuit = builder.finish(&FakeCompilation::default()); expect![[r#" @@ -233,18 +240,18 @@ fn target_qubit_trimmed_when_only_one_control_non_classical() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); - builder.qubit_allocate(&[], 2); - builder.qubit_allocate(&[], 3); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); + builder.qubit_allocate(&StackTrace::default(), 2); + builder.qubit_allocate(&StackTrace::default(), 3); - builder.gate(&[], "H", false, &[2], &[], None); - builder.gate(&[], "X", false, &[0], &[1, 2], None); - builder.gate(&[], "H", false, &[3], &[], None); - builder.gate(&[], "X", false, &[1], &[3, 2], None); - builder.measure(&[], "MResetZ", 0, &val::Result::Id(0)); - builder.measure(&[], "MResetZ", 1, &val::Result::Id(1)); - builder.measure(&[], "MResetZ", 2, &val::Result::Id(2)); + builder.gate(&StackTrace::default(), "H", false, &[2], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[0], &[1, 2], None); + builder.gate(&StackTrace::default(), "H", false, &[3], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[1], &[3, 2], None); + builder.measure(&StackTrace::default(), "MResetZ", 0, &val::Result::Id(0)); + builder.measure(&StackTrace::default(), "MResetZ", 1, &val::Result::Id(1)); + builder.measure(&StackTrace::default(), "MResetZ", 2, &val::Result::Id(2)); let circuit = builder.finish(&FakeCompilation::default()); @@ -270,18 +277,18 @@ fn controlled_paulis_become_uncontrolled_when_control_is_known_classical_one() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); - builder.qubit_allocate(&[], 2); - builder.qubit_allocate(&[], 3); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); + builder.qubit_allocate(&StackTrace::default(), 2); + builder.qubit_allocate(&StackTrace::default(), 3); - builder.gate(&[], "X", false, &[0], &[], None); - builder.gate(&[], "X", false, &[1], &[0], None); - builder.gate(&[], "Y", false, &[2], &[0], None); - builder.gate(&[], "Z", false, &[3], &[0], None); - builder.gate(&[], "H", false, &[1], &[], None); - builder.gate(&[], "H", false, &[2], &[], None); - builder.gate(&[], "H", false, &[3], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[1], &[0], None); + builder.gate(&StackTrace::default(), "Y", false, &[2], &[0], None); + builder.gate(&StackTrace::default(), "Z", false, &[3], &[0], None); + builder.gate(&StackTrace::default(), "H", false, &[1], &[], None); + builder.gate(&StackTrace::default(), "H", false, &[2], &[], None); + builder.gate(&StackTrace::default(), "H", false, &[3], &[], None); let circuit = builder.finish(&FakeCompilation::default()); @@ -305,14 +312,14 @@ fn ccx_becomes_cx_when_one_control_is_known_classical_one() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); - builder.qubit_allocate(&[], 2); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); + builder.qubit_allocate(&StackTrace::default(), 2); - builder.gate(&[], "X", false, &[0], &[], None); - builder.gate(&[], "H", false, &[1], &[], None); - builder.gate(&[], "H", false, &[2], &[], None); - builder.gate(&[], "X", false, &[2], &[0, 1], None); + builder.gate(&StackTrace::default(), "X", false, &[0], &[], None); + builder.gate(&StackTrace::default(), "H", false, &[1], &[], None); + builder.gate(&StackTrace::default(), "H", false, &[2], &[], None); + builder.gate(&StackTrace::default(), "X", false, &[2], &[0, 1], None); let circuit = builder.finish(&FakeCompilation::default()); @@ -336,13 +343,23 @@ fn ccx_becomes_cx_when_one_control_is_known_classical_one_with_grouping() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); - builder.qubit_allocate(&[], 2); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); + builder.qubit_allocate(&StackTrace::default(), 2); - builder.gate(&[c.user_code_frame("Main", 1)], "X", false, &[0], &[], None); builder.gate( - &[c.user_code_frame("Main", 2), c.user_code_frame("Foo", 3)], + &stack_trace(vec![c.user_code_frame("Main", 1)]), + "X", + false, + &[0], + &[], + None, + ); + builder.gate( + &stack_trace(vec![ + c.user_code_frame("Main", 2), + c.user_code_frame("Foo", 3), + ]), "H", false, &[1], @@ -350,7 +367,10 @@ fn ccx_becomes_cx_when_one_control_is_known_classical_one_with_grouping() { None, ); builder.gate( - &[c.user_code_frame("Main", 2), c.user_code_frame("Foo", 4)], + &stack_trace(vec![ + c.user_code_frame("Main", 2), + c.user_code_frame("Foo", 4), + ]), "H", false, &[2], @@ -358,7 +378,10 @@ fn ccx_becomes_cx_when_one_control_is_known_classical_one_with_grouping() { None, ); builder.gate( - &[c.user_code_frame("Main", 2), c.user_code_frame("Foo", 5)], + &stack_trace(vec![ + c.user_code_frame("Main", 2), + c.user_code_frame("Foo", 5), + ]), "X", false, &[2], @@ -388,12 +411,15 @@ fn group_with_no_remaining_operations_is_pruned() { &FakeCompilation::user_package_ids(), ); - builder.qubit_allocate(&[], 0); - builder.qubit_allocate(&[], 1); - builder.qubit_allocate(&[], 2); + builder.qubit_allocate(&StackTrace::default(), 0); + builder.qubit_allocate(&StackTrace::default(), 1); + builder.qubit_allocate(&StackTrace::default(), 2); builder.gate( - &[c.user_code_frame("Main", 1), c.user_code_frame("Bar", 3)], + &stack_trace(vec![ + c.user_code_frame("Main", 1), + c.user_code_frame("Bar", 3), + ]), "X", false, &[0], @@ -401,13 +427,19 @@ fn group_with_no_remaining_operations_is_pruned() { None, ); builder.measure( - &[c.user_code_frame("Main", 1), c.user_code_frame("Bar", 4)], + &stack_trace(vec![ + c.user_code_frame("Main", 1), + c.user_code_frame("Bar", 4), + ]), "M", 0, &val::Result::Id(0), ); builder.gate( - &[c.user_code_frame("Main", 2), c.user_code_frame("Foo", 3)], + &stack_trace(vec![ + c.user_code_frame("Main", 2), + c.user_code_frame("Foo", 3), + ]), "H", false, &[1], @@ -415,7 +447,10 @@ fn group_with_no_remaining_operations_is_pruned() { None, ); builder.gate( - &[c.user_code_frame("Main", 2), c.user_code_frame("Foo", 4)], + &stack_trace(vec![ + c.user_code_frame("Main", 2), + c.user_code_frame("Foo", 4), + ]), "H", false, &[2], @@ -423,7 +458,10 @@ fn group_with_no_remaining_operations_is_pruned() { None, ); builder.gate( - &[c.user_code_frame("Main", 2), c.user_code_frame("Foo", 5)], + &stack_trace(vec![ + c.user_code_frame("Main", 2), + c.user_code_frame("Foo", 5), + ]), "X", false, &[2], diff --git a/source/compiler/qsc_circuit/src/circuit.rs b/source/compiler/qsc_circuit/src/circuit.rs index 39de757ec3..ffb67b3519 100644 --- a/source/compiler/qsc_circuit/src/circuit.rs +++ b/source/compiler/qsc_circuit/src/circuit.rs @@ -4,7 +4,6 @@ #[cfg(test)] mod tests; -use qsc_fir::fir::PackageId; use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use std::{ @@ -358,27 +357,13 @@ pub struct Metadata { } #[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(untagged)] -pub enum SourceLocation { - Resolved(ResolvedSourceLocation), - #[serde(skip)] - Unresolved(PackageOffset), -} - -#[derive(Clone, Debug, Copy, PartialEq, Eq)] -pub struct PackageOffset { - pub package_id: PackageId, - pub offset: u32, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct ResolvedSourceLocation { +pub struct SourceLocation { pub file: String, pub line: u32, pub column: u32, } -impl Display for ResolvedSourceLocation { +impl Display for SourceLocation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{}:{}", self.file, self.line, self.column) } @@ -418,7 +403,7 @@ impl Row { fn add_measurement(&mut self, column: usize, source: Option<&SourceLocation>) { let mut gate_label = String::from("M"); if self.render_locations - && let Some(SourceLocation::Resolved(loc)) = source + && let Some(loc) = source { let _ = write!(&mut gate_label, "@{loc}"); } @@ -444,9 +429,9 @@ impl Row { } if self.render_locations - && let Some(SourceLocation::Resolved(loc)) = operation.source_location() + && let Some(loc) = operation.source_location() { - let _ = write!(&mut gate_label, "@{}:{}:{}", loc.file, loc.line, loc.column); + let _ = write!(&mut gate_label, "@{loc}"); } gate_label } @@ -741,15 +726,13 @@ impl CircuitDisplay<'_> { if self.render_locations { let mut first = true; for loc in &q.declarations { - if let SourceLocation::Resolved(loc) = loc { - if first { - label.push('@'); - first = false; - } else { - label.push_str(", "); - } - let _ = write!(&mut label, "{loc}"); + if first { + label.push('@'); + first = false; + } else { + label.push_str(", "); } + let _ = write!(&mut label, "{loc}"); } } rows.push(Row { diff --git a/source/compiler/qsc_eval/src/backend.rs b/source/compiler/qsc_eval/src/backend.rs index 526d0449b3..230c03f3e3 100644 --- a/source/compiler/qsc_eval/src/backend.rs +++ b/source/compiler/qsc_eval/src/backend.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::debug::Frame; +use crate::StackTrace; use crate::val::{self, Value}; use crate::{noise::PauliNoise, val::unwrap_tuple}; use ndarray::Array2; @@ -120,21 +120,21 @@ pub trait Backend { /// tracing is enabled. If stack tracing is disabled, the stack parameter /// will be ignored. pub trait Tracer { - fn qubit_allocate(&mut self, stack: &[Frame], q: usize); - fn qubit_release(&mut self, stack: &[Frame], q: usize); - fn qubit_swap_id(&mut self, stack: &[Frame], q0: usize, q1: usize); + fn qubit_allocate(&mut self, stack: &StackTrace, q: usize); + fn qubit_release(&mut self, stack: &StackTrace, q: usize); + fn qubit_swap_id(&mut self, stack: &StackTrace, q0: usize, q1: usize); fn gate( &mut self, - stack: &[Frame], + stack: &StackTrace, name: &str, is_adjoint: bool, targets: &[usize], controls: &[usize], theta: Option, ); - fn measure(&mut self, stack: &[Frame], name: &str, q: usize, r: &val::Result); - fn reset(&mut self, stack: &[Frame], q: usize); - fn custom_intrinsic(&mut self, stack: &[Frame], name: &str, arg: Value); + fn measure(&mut self, stack: &StackTrace, name: &str, q: usize, r: &val::Result); + fn reset(&mut self, stack: &StackTrace, q: usize); + fn custom_intrinsic(&mut self, stack: &StackTrace, name: &str, arg: Value); fn is_stack_tracing_enabled(&self) -> bool; } @@ -178,7 +178,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize, stack: &[Frame]) { + pub fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize, stack: &StackTrace) { if let OptionalBackend::Some(backend) = &mut self.backend { backend.ccx(ctl0, ctl1, q); } @@ -187,7 +187,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn cx(&mut self, ctl: usize, q: usize, stack: &[Frame]) { + pub fn cx(&mut self, ctl: usize, q: usize, stack: &StackTrace) { if let OptionalBackend::Some(backend) = &mut self.backend { backend.cx(ctl, q); } @@ -196,7 +196,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn cy(&mut self, ctl: usize, q: usize, stack: &[Frame]) { + pub fn cy(&mut self, ctl: usize, q: usize, stack: &StackTrace) { if let OptionalBackend::Some(backend) = &mut self.backend { backend.cy(ctl, q); } @@ -205,7 +205,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn cz(&mut self, ctl: usize, q: usize, stack: &[Frame]) { + pub fn cz(&mut self, ctl: usize, q: usize, stack: &StackTrace) { if let OptionalBackend::Some(backend) = &mut self.backend { backend.cz(ctl, q); } @@ -214,7 +214,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn h(&mut self, q: usize, stack: &[Frame]) { + pub fn h(&mut self, q: usize, stack: &StackTrace) { if let OptionalBackend::Some(backend) = &mut self.backend { backend.h(q); } @@ -223,7 +223,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn m(&mut self, q: usize, stack: &[Frame]) -> val::Result { + pub fn m(&mut self, q: usize, stack: &StackTrace) -> val::Result { let r = match &mut self.backend { OptionalBackend::Some(backend) => backend.m(q), OptionalBackend::None(fallback) => fallback.result_allocate(), @@ -234,7 +234,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { r } - pub fn mresetz(&mut self, q: usize, stack: &[Frame]) -> val::Result { + pub fn mresetz(&mut self, q: usize, stack: &StackTrace) -> val::Result { let r = match &mut self.backend { OptionalBackend::Some(backend) => backend.mresetz(q), OptionalBackend::None(fallback) => fallback.result_allocate(), @@ -245,7 +245,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { r } - pub fn reset(&mut self, q: usize, stack: &[Frame]) { + pub fn reset(&mut self, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.reset(stack, q); } @@ -254,7 +254,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn rx(&mut self, theta: f64, q: usize, stack: &[Frame]) { + pub fn rx(&mut self, theta: f64, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Rx", false, &[q], &[], Some(theta)); } @@ -263,7 +263,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn rxx(&mut self, theta: f64, q0: usize, q1: usize, stack: &[Frame]) { + pub fn rxx(&mut self, theta: f64, q0: usize, q1: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Rxx", false, &[q0, q1], &[], Some(theta)); } @@ -272,7 +272,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn ry(&mut self, theta: f64, q: usize, stack: &[Frame]) { + pub fn ry(&mut self, theta: f64, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Ry", false, &[q], &[], Some(theta)); } @@ -281,7 +281,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn ryy(&mut self, theta: f64, q0: usize, q1: usize, stack: &[Frame]) { + pub fn ryy(&mut self, theta: f64, q0: usize, q1: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Ryy", false, &[q0, q1], &[], Some(theta)); } @@ -290,7 +290,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn rz(&mut self, theta: f64, q: usize, stack: &[Frame]) { + pub fn rz(&mut self, theta: f64, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Rz", false, &[q], &[], Some(theta)); } @@ -299,7 +299,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn rzz(&mut self, theta: f64, q0: usize, q1: usize, stack: &[Frame]) { + pub fn rzz(&mut self, theta: f64, q0: usize, q1: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Rzz", false, &[q0, q1], &[], Some(theta)); } @@ -308,7 +308,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn sadj(&mut self, q: usize, stack: &[Frame]) { + pub fn sadj(&mut self, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "S", true, &[q], &[], None); } @@ -317,7 +317,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn s(&mut self, q: usize, stack: &[Frame]) { + pub fn s(&mut self, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "S", false, &[q], &[], None); } @@ -326,7 +326,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn sx(&mut self, q: usize, stack: &[Frame]) { + pub fn sx(&mut self, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "SX", false, &[q], &[], None); } @@ -335,7 +335,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn swap(&mut self, q0: usize, q1: usize, stack: &[Frame]) { + pub fn swap(&mut self, q0: usize, q1: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "SWAP", false, &[q0, q1], &[], None); } @@ -344,7 +344,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn tadj(&mut self, q: usize, stack: &[Frame]) { + pub fn tadj(&mut self, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "T", true, &[q], &[], None); } @@ -353,7 +353,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn t(&mut self, q: usize, stack: &[Frame]) { + pub fn t(&mut self, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "T", false, &[q], &[], None); } @@ -362,7 +362,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn x(&mut self, q: usize, stack: &[Frame]) { + pub fn x(&mut self, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "X", false, &[q], &[], None); } @@ -371,7 +371,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn y(&mut self, q: usize, stack: &[Frame]) { + pub fn y(&mut self, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Y", false, &[q], &[], None); } @@ -380,7 +380,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn z(&mut self, q: usize, stack: &[Frame]) { + pub fn z(&mut self, q: usize, stack: &StackTrace) { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Z", false, &[q], &[], None); } @@ -389,7 +389,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn qubit_allocate(&mut self, stack: &[Frame]) -> usize { + pub fn qubit_allocate(&mut self, stack: &StackTrace) -> usize { let q = match &mut self.backend { OptionalBackend::Some(backend) => backend.qubit_allocate(), OptionalBackend::None(fallback) => fallback.qubit_allocate(), @@ -400,7 +400,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { q } - pub fn qubit_release(&mut self, q: usize, stack: &[Frame]) -> bool { + pub fn qubit_release(&mut self, q: usize, stack: &StackTrace) -> bool { let b = match &mut self.backend { OptionalBackend::Some(backend) => backend.qubit_release(q), OptionalBackend::None(fallback) => fallback.qubit_release(q), @@ -411,7 +411,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { b } - pub fn qubit_swap_id(&mut self, q0: usize, q1: usize, stack: &[Frame]) { + pub fn qubit_swap_id(&mut self, q0: usize, q1: usize, stack: &StackTrace) { if let OptionalBackend::Some(backend) = &mut self.backend { backend.qubit_swap_id(q0, q1); } @@ -440,7 +440,7 @@ impl<'a, B: Backend> TracingBackend<'a, B> { &mut self, name: &str, arg: Value, - stack: &[Frame], + stack: &StackTrace, ) -> Option> { if let Some(tracer) = &mut self.tracer { tracer.custom_intrinsic(stack, name, arg.clone()); diff --git a/source/compiler/qsc_eval/src/intrinsic.rs b/source/compiler/qsc_eval/src/intrinsic.rs index 5047bae140..ed57064b7f 100644 --- a/source/compiler/qsc_eval/src/intrinsic.rs +++ b/source/compiler/qsc_eval/src/intrinsic.rs @@ -7,9 +7,8 @@ mod utils; mod tests; use crate::{ - Error, Rc, + Error, Rc, StackTrace, backend::{Backend, TracingBackend}, - debug::Frame, error::PackageSpan, output::Receiver, val::{self, Value, unwrap_tuple}, @@ -26,7 +25,7 @@ pub(crate) fn call( name_span: PackageSpan, arg: Value, arg_span: PackageSpan, - call_stack: &[Frame], + call_stack: &StackTrace, sim: &mut TracingBackend<'_, B>, rng: &mut StdRng, out: &mut dyn Receiver, diff --git a/source/compiler/qsc_eval/src/lib.rs b/source/compiler/qsc_eval/src/lib.rs index 799f72903b..1c00ff8c3f 100644 --- a/source/compiler/qsc_eval/src/lib.rs +++ b/source/compiler/qsc_eval/src/lib.rs @@ -39,9 +39,9 @@ use output::Receiver; use qsc_data_structures::{functors::FunctorApp, index_map::IndexMap, span::Span}; use qsc_fir::fir::{ self, BinOp, BlockId, CallableImpl, ConfiguredExecGraph, ExecGraph, ExecGraphConfig, - ExecGraphNode, Expr, ExprId, ExprKind, Field, FieldAssign, Global, Lit, LocalItemId, - LocalVarId, PackageId, PackageStoreLookup, PatId, PatKind, PrimField, Res, StmtId, StoreItemId, - StringComponent, UnOp, + ExecGraphDebugNode, ExecGraphNode, Expr, ExprId, ExprKind, Field, FieldAssign, Global, Lit, + LocalItemId, LocalVarId, PackageId, PackageStoreLookup, PatId, PatKind, PrimField, Res, StmtId, + StoreItemId, StringComponent, UnOp, }; use qsc_fir::ty::Ty; use qsc_lowerer::map_fir_package_to_hir; @@ -461,6 +461,19 @@ impl Env { self.scopes.push(scope); } + pub fn push_loop_scope(&mut self, frame_id: usize, loop_expr: ExprId) { + let iteration_count = 0; + let scope = Scope { + frame_id, + loop_scope: Some(LoopScope { + loop_expr, + iteration_count, + }), + ..Default::default() + }; + self.scopes.push(scope); + } + pub fn leave_scope(&mut self) { // Only pop the scope if there is more than one scope in the stack, // because the global/top-level scope cannot be exited. @@ -549,10 +562,17 @@ impl Env { } } -#[derive(Default)] -struct Scope { +#[derive(Default, Clone)] +pub struct Scope { bindings: IndexMap, - frame_id: usize, + pub frame_id: usize, + pub loop_scope: Option, +} + +#[derive(Default, Clone)] +pub struct LoopScope { + pub loop_expr: ExprId, + pub iteration_count: usize, } type CallableCountKey = (StoreItemId, bool, bool); @@ -583,6 +603,37 @@ pub struct State { exec_graph_config: ExecGraphConfig, } +#[derive(Default)] +pub struct StackTrace { + call_stack: Vec, + scope_stack: Vec, +} + +impl StackTrace { + #[must_use] + pub fn new(call_stack: Vec, scope_stack: Vec) -> Self { + Self { + call_stack, + scope_stack, + } + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.call_stack.is_empty() && self.scope_stack.is_empty() + } + + #[must_use] + pub fn call_stack(&self) -> &Vec { + &self.call_stack + } + + #[must_use] + pub fn scope_stack(&self) -> &Vec { + &self.scope_stack + } +} + impl State { #[must_use] pub fn new( @@ -651,6 +702,10 @@ impl State { env.push_scope(self.current_frame_id()); } + fn push_loop_scope(&mut self, env: &mut Env, loop_expr: ExprId) { + env.push_loop_scope(self.current_frame_id(), loop_expr); + } + fn take_val_register(&mut self) -> Value { self.val_register.take().expect("value should be present") } @@ -698,11 +753,12 @@ impl State { pub fn capture_stack_if_trace_enabled( &self, tracing_backend: &TracingBackend<'_, B>, - ) -> Vec { + env: &Env, + ) -> StackTrace { if tracing_backend.is_stacks_enabled() { - self.capture_stack() + StackTrace::new(self.capture_stack(), env.scopes.clone()) } else { - vec![] + StackTrace::default() } } @@ -766,15 +822,6 @@ impl State { } } } - Some(ExecGraphNode::Stmt(stmt)) => { - self.idx += 1; - self.current_span = globals.get_stmt((self.package, *stmt).into()).span; - - match self.check_for_break(breakpoints, *stmt, step, current_frame) { - Some(value) => value, - None => continue, - } - } Some(ExecGraphNode::Jump(idx)) => { self.idx = *idx; continue; @@ -812,31 +859,57 @@ impl State { env.leave_scope(); continue; } - Some(ExecGraphNode::RetFrame) => { - self.leave_frame(); - env.leave_current_frame(); - continue; - } - Some(ExecGraphNode::PushScope) => { - self.push_scope(env); - self.idx += 1; - continue; - } - Some(ExecGraphNode::PopScope) => { - env.leave_scope(); - self.idx += 1; - continue; - } - Some(ExecGraphNode::BlockEnd(id)) => { - self.idx += 1; - match self.check_for_block_exit_break(globals, *id, step, current_frame) { - Some((result, span)) => { - self.current_span = span; - return Ok(result); + Some(ExecGraphNode::Debug(dbg_node)) => match dbg_node { + ExecGraphDebugNode::PushScope => { + self.push_scope(env); + self.idx += 1; + continue; + } + ExecGraphDebugNode::PushLoopScope(expr) => { + self.push_loop_scope(env, *expr); + self.idx += 1; + continue; + } + ExecGraphDebugNode::RetFrame => { + self.leave_frame(); + env.leave_current_frame(); + continue; + } + ExecGraphDebugNode::LoopIteration => { + // we're in an iteration, increment counter + let current_scope = env.scopes.last_mut().expect("last scope should exist"); + let Some(loop_scope) = &mut current_scope.loop_scope.as_mut() else { + panic!("expected to be in a loop scope"); + }; + loop_scope.iteration_count += 1; + self.idx += 1; + continue; + } + ExecGraphDebugNode::PopScope => { + env.leave_scope(); + self.idx += 1; + continue; + } + ExecGraphDebugNode::BlockEnd(id) => { + self.idx += 1; + match self.check_for_block_exit_break(globals, *id, step, current_frame) { + Some((result, span)) => { + self.current_span = span; + return Ok(result); + } + None => continue, } - None => continue, } - } + ExecGraphDebugNode::Stmt(stmt) => { + self.idx += 1; + self.current_span = globals.get_stmt((self.package, *stmt).into()).span; + + match self.check_for_break(breakpoints, *stmt, step, current_frame) { + Some(value) => value, + None => continue, + } + } + }, None => { // We have reached the end of the current graph without reaching an explicit return node, // usually indicating the partial execution of a single sub-expression. @@ -1280,7 +1353,7 @@ impl State { arg_span: PackageSpan, out: &mut impl Receiver, ) -> Result<(), Error> { - let call_stack = self.capture_stack_if_trace_enabled(sim); + let call_stack = self.capture_stack_if_trace_enabled(sim, env); self.push_frame(Vec::new().into(), callee_id, functor); self.current_span = callee_span.span; self.increment_call_count(callee_id, functor); diff --git a/source/compiler/qsc_fir/src/fir.rs b/source/compiler/qsc_fir/src/fir.rs index a4633ae823..6fe3503d15 100644 --- a/source/compiler/qsc_fir/src/fir.rs +++ b/source/compiler/qsc_fir/src/fir.rs @@ -1036,18 +1036,29 @@ pub enum ExecGraphNode { Unit, /// The end of the control flow graph. Ret, - /// The end of the control flow graph plus a pop of the current debug frame. Used instead of `Ret` - /// when debugging. - RetFrame, + /// A node only to be executed in debug mode. + Debug(ExecGraphDebugNode), +} + +#[derive(Copy, Clone, Debug, PartialEq)] +/// A debug-only node within the control flow graph. +pub enum ExecGraphDebugNode { /// A statement to track for debugging. Stmt(StmtId), - /// A push of a new scope, used when tracking variables for debugging. + /// A push of a new scope. PushScope, - /// A pop of the current scope, used when tracking variables for debugging. + /// A push of a new loop scope. The `ExprId` is the condition or iterable expression. + PushLoopScope(ExprId), + /// A pop of the current scope. Loop scopes are also popped with this node. PopScope, /// The end of a block, used in debugging to have a step point after all statements in a block have been executed, /// but before the block is exited. BlockEnd(BlockId), + /// The end of the control flow graph plus a pop of the current debug frame. Used instead of `Ret` + /// when debugging. + RetFrame, + /// The beginning of a loop iteration. + LoopIteration, } /// A sequenced block of statements. diff --git a/source/compiler/qsc_lowerer/src/lib.rs b/source/compiler/qsc_lowerer/src/lib.rs index 3aba8a2cca..339662fbe8 100644 --- a/source/compiler/qsc_lowerer/src/lib.rs +++ b/source/compiler/qsc_lowerer/src/lib.rs @@ -4,7 +4,8 @@ use qsc_data_structures::index_map::IndexMap; use qsc_fir::assigner::Assigner; use qsc_fir::fir::{ - Block, CallableImpl, ExecGraph, ExecGraphIdx, ExecGraphNode, Expr, Pat, SpecImpl, Stmt, + Block, CallableImpl, ExecGraph, ExecGraphDebugNode, ExecGraphIdx, ExecGraphNode, Expr, Pat, + SpecImpl, Stmt, }; use qsc_fir::{ fir::{self, BlockId, ExprId, LocalItemId, PatId, StmtId}, @@ -55,7 +56,7 @@ impl ExecGraphBuilder { let debug_exec_graph = self .debug .drain(..) - .chain(once(ExecGraphNode::RetFrame)) + .chain(once(ExecGraphNode::Debug(ExecGraphDebugNode::RetFrame))) .collect::>() .into(); @@ -70,8 +71,8 @@ impl ExecGraphBuilder { } /// Pushes a node to *only* the debug execution graph. - fn debug_push(&mut self, node: ExecGraphNode) { - self.debug.push(node); + fn debug_push(&mut self, node: ExecGraphDebugNode) { + self.debug.push(ExecGraphNode::Debug(node)); } /// Pushes a node to the execution graph. @@ -99,7 +100,8 @@ impl ExecGraphBuilder { /// Pushes a return node to the execution graph. fn push_ret(&mut self) { self.no_debug.push(ExecGraphNode::Ret); - self.debug.push(ExecGraphNode::RetFrame); + self.debug + .push(ExecGraphNode::Debug(ExecGraphDebugNode::RetFrame)); } /// Returns the current length of the execution graph. @@ -400,7 +402,7 @@ impl Lowerer { panic!("if a SpecDecl is some, then it must be an implementation"); }; let input = pat.as_ref().map(|p| self.lower_spec_decl_pat(p)); - let block = self.lower_block(block); + let block = self.lower_block(block, false); fir::SpecDecl { id: self.lower_id(decl.id), span: decl.span, @@ -429,7 +431,7 @@ impl Lowerer { id } - fn lower_block(&mut self, block: &hir::Block) -> BlockId { + fn lower_block(&mut self, block: &hir::Block, is_loop_body: bool) -> BlockId { let id = self.assigner.next_block(); // When lowering for debugging, we need to be more strict about scoping for variables // otherwise variables that are not in scope will be visible in the locals view. @@ -440,7 +442,15 @@ impl Lowerer { // is performed via their lowered local variable ID, so they cannot be accessed outside of // their scope. Associated memory is still cleaned up at callable exit rather than block // exit. - self.exec_graph.debug_push(ExecGraphNode::PushScope); + + if is_loop_body { + // Increment iteration counter for loop body scopes + self.exec_graph + .debug_push(ExecGraphDebugNode::LoopIteration); + } + + self.exec_graph.debug_push(ExecGraphDebugNode::PushScope); + let set_unit = block.stmts.is_empty() || !matches!( block.stmts.last().expect("block should be non-empty").kind, @@ -455,8 +465,8 @@ impl Lowerer { if set_unit { self.exec_graph.push(ExecGraphNode::Unit); } - self.exec_graph.debug_push(ExecGraphNode::BlockEnd(id)); - self.exec_graph.debug_push(ExecGraphNode::PopScope); + self.exec_graph.debug_push(ExecGraphDebugNode::BlockEnd(id)); + self.exec_graph.debug_push(ExecGraphDebugNode::PopScope); self.blocks.insert(id, block); id } @@ -464,7 +474,8 @@ impl Lowerer { fn lower_stmt(&mut self, stmt: &hir::Stmt) -> fir::StmtId { let id = self.assigner.next_stmt(); let graph_start_idx = self.exec_graph.len(); - self.exec_graph.debug_push(ExecGraphNode::Stmt(id)); + + self.exec_graph.debug_push(ExecGraphDebugNode::Stmt(id)); let kind = match &stmt.kind { hir::StmtKind::Expr(expr) => fir::StmtKind::Expr(self.lower_expr(expr)), hir::StmtKind::Item(item) => fir::StmtKind::Item(lower_local_item_id(*item)), @@ -628,7 +639,7 @@ impl Lowerer { } fir::ExprKind::BinOp(lower_binop(*op), lhs, rhs) } - hir::ExprKind::Block(block) => fir::ExprKind::Block(self.lower_block(block)), + hir::ExprKind::Block(block) => fir::ExprKind::Block(self.lower_block(block, false)), hir::ExprKind::Call(callee, arg) => { let call = self.lower_expr(callee); self.exec_graph.push(ExecGraphNode::Store); @@ -727,16 +738,22 @@ impl Lowerer { fir::ExprKind::UnOp(lower_unop(*op), self.lower_expr(operand)) } hir::ExprKind::While(cond, body) => { + self.exec_graph + .debug_push(ExecGraphDebugNode::PushLoopScope(id)); + let cond_idx = self.exec_graph.len(); let cond = self.lower_expr(cond); let idx = self.exec_graph.len(); // Put a placeholder in the execution graph for the jump past the loop self.exec_graph.push(ExecGraphNode::Jump(0)); - let body = self.lower_block(body); + let body = self.lower_block(body, true); self.exec_graph.push_with_arg(ExecGraphNode::Jump, cond_idx); // Update the placeholder to skip the loop if the condition is false self.exec_graph .set_with_arg(ExecGraphNode::JumpIfNot, idx, self.exec_graph.len()); + + self.exec_graph.debug_push(ExecGraphDebugNode::PopScope); + // While-exprs never have a return value, so we need to insert a no-op to ensure // a Unit value is returned for the expr. self.exec_graph.push(ExecGraphNode::Unit); diff --git a/source/npm/qsharp/test/circuits-cases/loops.qs b/source/npm/qsharp/test/circuits-cases/loops.qs new file mode 100644 index 0000000000..8bcae3eb12 --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/loops.qs @@ -0,0 +1,60 @@ +operation Main() : Result[] { + let N : Int = 9; + let evolutionTime : Double = 4.0; + let numberOfSteps : Int = 7; + let J : Double = 1.0; + let g : Double = 0.7; + IsingModel1DEvolution(N, J, g, evolutionTime, numberOfSteps) +} + +/// # Summary +/// Simulate simple Ising model evolution +/// +/// # Description +/// Simulates state |𝜓⟩ evolution to find |𝜓(t)⟩=U(t)|𝜓(0)⟩. +/// |𝜓(0)⟩ is taken to be |0...0⟩. +/// U(t)=e⁻ⁱᴴᵗ, where H is an Ising model Hamiltonian H = -J·Σ'ᵢⱼZᵢZⱼ + g·ΣᵢXᵢ +/// Here Σ' is taken over all pairs of neighboring qubits . +/// Simulation is done by performing K steps assuming U(t)≈(U(t/K))ᴷ. +operation IsingModel1DEvolution( + N : Int, + J : Double, + g : Double, + evolutionTime : Double, + numberOfSteps : Int +) : Result[] { + + // Allocate qubit grid + use qubits = Qubit[N]; + + // Compute the time step + let dt : Double = evolutionTime / Std.Convert.IntAsDouble(numberOfSteps); + + let theta_x = - g * dt; + let theta_zz = J * dt; + + // Perform K steps + for i in 1..numberOfSteps { + + // Single-qubit interaction with external field + for q in qubits { + Rx(2.0 * theta_x, q); + } + + // All of the following Rzz gates commute. So we apply them between "even" + // pairs first and then between "odd" pairs to reduce the algorithm depth. + + // Interactions between "even" pairs + for j in 0..2..N-2 { + Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + } + + // Interactions between "odd" pairs + for j in 1..2..N-2 { + Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + } + + } + + MResetEachZ(qubits) +} diff --git a/source/npm/qsharp/test/circuits-cases/loops.qs.snapshot.html b/source/npm/qsharp/test/circuits-cases/loops.qs.snapshot.html new file mode 100644 index 0000000000..2bc9835ccf --- /dev/null +++ b/source/npm/qsharp/test/circuits-cases/loops.qs.snapshot.html @@ -0,0 +1,6989 @@ + + + + + + + +

circuit-eval-collapsed

+
+ + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 0 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 1 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 2 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 3 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 4 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 5 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 6 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 7 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 8 + ⟩ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + loops.qs:37:5 for i in 1..numberOfSteps { + + + + + loop + : 1.. + numberOfSteps + + + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + + + + + + + + + + + + +
+

circuit-eval-expanded

+
+ + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 0 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 1 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 2 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 3 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 4 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 5 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 6 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 7 + ⟩ + + + + loops.qs:28:5 use qubits = Qubit[N]; + + | + ψ + 8 + ⟩ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + loops.qs:41:13 Rx(2.0 * theta_x, q); + + + + + Rx + + + -0.8000 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:49:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + + loops.qs:54:13 Rzz(2.0 * theta_zz, qubits[j], qubits[j + 1]); + + + + + + Rzz + + + 1.1429 + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + + loops.qs:59:5 MResetEachZ(qubits) + + + + + |0⟩ + + + + + + + +
+ + diff --git a/source/npm/qsharp/test/circuits-cases/nested-callables.qs b/source/npm/qsharp/test/circuits-cases/nested-callables.qs index 2db05695fe..d874cf2979 100644 --- a/source/npm/qsharp/test/circuits-cases/nested-callables.qs +++ b/source/npm/qsharp/test/circuits-cases/nested-callables.qs @@ -1,4 +1,8 @@ operation Main() : Unit { + SingleTopLevelOperation(); +} + +operation SingleTopLevelOperation() : Unit { use q1 = Qubit(); use q2 = Qubit(); Bar(q1); @@ -10,6 +14,7 @@ operation Main() : Unit { Bar(q2); Foo(q2); } + operation Foo(q : Qubit) : Unit { Bar(q); MResetZ(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 index 78b17210b3..e920598221 100644 --- a/source/npm/qsharp/test/circuits-cases/nested-callables.qs.snapshot.html +++ b/source/npm/qsharp/test/circuits-cases/nested-callables.qs.snapshot.html @@ -13,14 +13,14 @@ - nested-callables.qs:2:5 use q1 = Qubit(); + nested-callables.qs:6:5 use q1 = Qubit(); - nested-callables.qs:3:5 use q2 = Qubit(); + nested-callables.qs:7:5 use q2 = Qubit(); - - + + - - nested-callables.qs:4:5 Bar(q1); - - - - - Bar - + + + + + nested-callables.qs:8:5 Bar(q1); + + + + + Bar + + + + + + + - - - - - - - - - nested-callables.qs:5:5 Bar(q2); - - - - - Bar - + + + nested-callables.qs:9:5 Bar(q2); + + + + + Bar + + + + + + + - - - - - - - - - nested-callables.qs:6:5 Foo(q1); - - - - - Foo - + + + nested-callables.qs:10:5 Foo(q1); + + + + + Foo + + + + + + + - - - - - - - - - nested-callables.qs:9:5 Foo(q2); - - - + + nested-callables.qs:13:5 Foo(q2); + + + + + Foo + + + + + + + - - Foo - - - - - - - - - - nested-callables.qs:7:5 Bar(q1); - - - - - Bar - + + + nested-callables.qs:11:5 Bar(q1); + + + + + Bar + + + + + + + - - - - - - - - - nested-callables.qs:10:5 Bar(q2); - - - - - Bar - + + + nested-callables.qs:14:5 Bar(q2); + + + + + Bar + + + + + + + - - - - - - - - - nested-callables.qs:8:5 Foo(q1); - - - - - - Foo - - - - - - Foo - + + + nested-callables.qs:12:5 Foo(q1); + + + + + + Foo + + + + + + Foo + + + + + + + - - - - - - - - - nested-callables.qs:11:5 Foo(q2); - - - - - - Foo - - - - + + nested-callables.qs:15:5 Foo(q2); + + + + + + Foo + + + + + + Foo + + + + + + + - - Foo - - - - - + + + + @@ -493,7 +520,7 @@ > - nested-callables.qs:2:5 use q1 = Qubit(); + nested-callables.qs:6:5 use q1 = Qubit(); - nested-callables.qs:3:5 use q2 = Qubit(); + nested-callables.qs:7:5 use q2 = Qubit(); - nested-callables.qs:18:5 X(q); + nested-callables.qs:23:5 X(q); @@ -669,12 +696,12 @@ - nested-callables.qs:18:5 X(q); + nested-callables.qs:23:5 X(q); @@ -684,12 +711,12 @@ - nested-callables.qs:19:5 Y(q); + nested-callables.qs:24:5 Y(q); - nested-callables.qs:19:5 Y(q); + nested-callables.qs:24:5 Y(q); - nested-callables.qs:18:5 X(q); + nested-callables.qs:23:5 X(q); @@ -751,12 +778,12 @@ - nested-callables.qs:18:5 X(q); + nested-callables.qs:23:5 X(q); @@ -766,12 +793,12 @@ - nested-callables.qs:19:5 Y(q); + nested-callables.qs:24:5 Y(q); - nested-callables.qs:19:5 Y(q); + nested-callables.qs:24:5 Y(q); - nested-callables.qs:15:5 MResetZ(q); + nested-callables.qs:20:5 MResetZ(q); - nested-callables.qs:15:5 MResetZ(q); + nested-callables.qs:20:5 MResetZ(q); - nested-callables.qs:15:5 MResetZ(q); + nested-callables.qs:20:5 MResetZ(q); - nested-callables.qs:15:5 MResetZ(q); + nested-callables.qs:20:5 MResetZ(q); - nested-callables.qs:18:5 X(q); + nested-callables.qs:23:5 X(q); @@ -963,12 +990,12 @@ - nested-callables.qs:18:5 X(q); + nested-callables.qs:23:5 X(q); @@ -978,12 +1005,12 @@ - nested-callables.qs:19:5 Y(q); + nested-callables.qs:24:5 Y(q); - nested-callables.qs:19:5 Y(q); + nested-callables.qs:24:5 Y(q); - nested-callables.qs:18:5 X(q); + nested-callables.qs:23:5 X(q); @@ -1045,12 +1072,12 @@ - nested-callables.qs:18:5 X(q); + nested-callables.qs:23:5 X(q); @@ -1060,12 +1087,12 @@ - nested-callables.qs:19:5 Y(q); + nested-callables.qs:24:5 Y(q); - nested-callables.qs:19:5 Y(q); + nested-callables.qs:24:5 Y(q); - nested-callables.qs:15:5 MResetZ(q); + nested-callables.qs:20:5 MResetZ(q); - nested-callables.qs:15:5 MResetZ(q); + nested-callables.qs:20:5 MResetZ(q); - nested-callables.qs:15:5 MResetZ(q); + nested-callables.qs:20:5 MResetZ(q); - nested-callables.qs:15:5 MResetZ(q); + nested-callables.qs:20:5 MResetZ(q);