Skip to content

Commit 65c9d96

Browse files
minestarksswernli
andauthored
Circuit diagrams: Group loops (#2827)
## Feature changes Loops from user source are now grouped in circuit diagrams. **BEFORE:** <img width="980" height="397" alt="image" src="https://github.com/user-attachments/assets/4051a229-0911-4f2f-b1d4-84a0eb214520" /> ```plaintext [email protected]:4:20 ─ [email protected]:6:24 ─── [email protected]:6:24 ─── [email protected]:6:24 ── ``` **AFTER:** <img width="481" height="776" alt="image" src="https://github.com/user-attachments/assets/fa839ef3-4385-439a-ace8-3bfe0d846adf" /> <img width="937" height="770" alt="image" src="https://github.com/user-attachments/assets/5bc304de-fcea-43bc-ad32-6114c75d98ea" /> ```plaintext [email protected]:4:20 ─ [ [Main] ─── [ [loop: [email protected]:5:20] ── [ [(1)@test.qs:5:34] ─── [ [[email protected]:6:24] ─── [email protected]:11:20 ── [email protected]:12:20 ─── ] ──── ] ─── [ [(2)@test.qs:5:34] ─── [ [[email protected]:6:24] ─── [email protected]:11:20 ── [email protected]:12:20 ─── ] ──── ] ─── [ [(3)@test.qs:5:34] ─── [ [[email protected]:6:24] ─── [email protected]:11:20 ── [email protected]:12:20 ─── ] ──── ] ──── ] ──── ] ── ``` - All loops (`for`, `while`, `repeat`/`until`) are supported. The label is the expression in the loop header, which can be, depending on the loop type, an iterable, a "while" condition, or an "until" condition. e.g.: - `loop: 0..2` - `loop: i < 2` - `loop: i==2` - Exception 1: If a loop only has a single iteration, we don't group. - Exception 2: If a loop is "vertical", meaning each iteration of the loop only interacts with a distinct set of qubits, then there is no grouping, since grouping in this case tends to look more confusing than helpful. ## Internals ### High level circuit diagram pipeline To recap, this is how circuit diagrams get currently generated. **Q#/OpenQASM** gets compiled to (via the compilation pipeline --`qsc_frontend`, `qsc_lowererer` FIR lowerer etc)... ...**FIR** gets evaluated and traced (via `qsc_eval` Evaluator)... ...**Traces with raw stacks** get captured and transformed to (via Circuit Tracer)... ...**Traces with logical stacks** get transformed to (via Circuit Tracer)... ...**`Circuit` representation** gets serialized to (via `serde-json`)... ...**Circuit JSON object** gets rendered as (via `circuit-vis`) ...**SVG & HTML circuit diagram** ### Evaluator changes - Tracing in the Evaluator: The `Tracer` now captures the stack of `Scope`s in addition to the usual call stack (`Frame`s) in the tracing calls. The scope stack includes the stack of current lexical scopes and loops, including iteration count. This allows the circuit tracer to build a comprehensive "logical" stack that includes both call frames and lexical scopes in the source. - The evaluator, when in DEBUG configuration, pushes LOOPs as a scope into the `Scope` stack. Before this change, we did track each Block as a scope, but not specifically loops. So now when we're in a loop, there is an extra `Scope` in the stack: ```plaintext > BEGIN LOOP SCOPE > for i in 0..4 > > BEGIN BLOCK SCOPE > { > H(qs[i]); > } > END BLOCK SCOPE > > END LOOP SCOPE ``` - The loop scope is also associated with an `iteration_count` which the evaluator is instructed to increment every time it enters the body block of the loop. ### FIR lowerer changes - FIR: In the execution graph, debug-only nodes are consolidated into a separate `ExecGraphDebugNode` enum, just for legibility. - FIR: during lowering, we add some extra data to the execution graph for loops: The a new `PushLoopScope` instruction to push loop scope, with an attached `ExprId` (used later by the circuit tracer to look up the loop source code and display the loop label) , and a `LoopIteration` instruction that is used to indicate a new iteration of the loop has started. ### Circuit tracer changes - `builder.rs` / `CircuitTracer`: Introduces a `LogicalStack` which is a blended stack of call frames and lexical scopes. This struct is produced by transforming the stack traces passed down from the evaluator into a more "friendly" shape, and it corresponds to the structure that will ultimately be seen in the circuit diagram. - `circuit.rs` / `Circuit` : The `SourceLocation::Unresolved` enum is a now-unnecessary abstraction that is removed in this change. - in `CircuitTracer`, `finish` now takes both FIR and HIR store to resolve sources and scopes ### `circuit-vis` HTML renderer - In the renderer, we now automatically expand any groups that only contain a single operation, which saves the user having to manually expand multiple levels of operations. ## Test coverage - Native interpreter tests in `compiler/qsc/src/interpret/circuit_tests.rs` test direct all the way from Q# to the `Circuit` representation - Snapshot tests in `npm/qsharp/test/circuits-cases` test all the way from Q# to rendered SVG & HTML circuit diagram using the JS/WASM component - `compiler/qsc_circuit/src/builder/tests/logical_stack_trace.rs` - this exercises the Q# -> traces pipeline, validating that the evaluator returns expected "logical" stack traces --------- Co-authored-by: Stefan J. Wernli <[email protected]>
1 parent f7d15c5 commit 65c9d96

File tree

15 files changed

+10252
-1410
lines changed

15 files changed

+10252
-1410
lines changed

source/compiler/qsc/src/interpret.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,7 @@ impl Interpreter {
848848
self.circuit_tracer
849849
.as_ref()
850850
.expect("to call get_circuit, the interpreter should be initialized with circuit tracing enabled")
851-
.snapshot(self.compiler.package_store())
851+
.snapshot(&(self.compiler.package_store(), &self.fir_store))
852852
}
853853

854854
/// Performs QIR codegen using the given entry expression on a new instance of the environment
@@ -1022,7 +1022,7 @@ impl Interpreter {
10221022
}
10231023
}
10241024
}
1025-
let circuit = tracer.finish(self.compiler.package_store());
1025+
let circuit = tracer.finish(&(self.compiler.package_store(), &self.fir_store));
10261026
Ok(circuit)
10271027
}
10281028

source/compiler/qsc/src/interpret/circuit_tests.rs

Lines changed: 270 additions & 10 deletions
Large diffs are not rendered by default.

source/compiler/qsc/src/interpret/debug.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub(crate) fn format_call_stack(
4949
write!(trace, "{}", call.name.name).expect("writing to string should succeed");
5050

5151
let name = get_item_file_name(store, frame.id);
52-
let pos = get_position(frame, store);
52+
let pos = get_position(&frame, store);
5353
write!(
5454
trace,
5555
" in {}:{}:{}",
@@ -99,7 +99,7 @@ fn get_ns_name(item: &Item) -> Option<Rc<str>> {
9999
}
100100

101101
/// Converts the [`Span`] of [`Frame`] into a [`Position`].
102-
fn get_position(frame: Frame, store: &PackageStore) -> Position {
102+
fn get_position(frame: &Frame, store: &PackageStore) -> Position {
103103
let filename = get_item_file_name(store, frame.id).expect("file should exist");
104104
let package_id = map_fir_package_to_hir(frame.id.package);
105105
let unit = store.get(package_id).expect("package should exist");

0 commit comments

Comments
 (0)