Skip to content

Conversation

@minestarks
Copy link
Member

@minestarks minestarks commented Dec 31, 2025

Feature changes

Loops from user source are now grouped in circuit diagrams.

BEFORE:

image
[email protected]:4:20 ─ [email protected]:6:24 ─── [email protected]:6:24 ─── [email protected]:6:24 ──

AFTER:

image image
[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 Scopes in addition to the usual call stack (Frames) 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:


>  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

Base automatically changed from minestarks/source-links-for-groups to main January 7, 2026 19:42
@minestarks minestarks marked this pull request as ready for review January 8, 2026 22:47
// 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is the only place that is_loop_body is passed as true, and that only causes the extra node to be pushed, it might make sense to put the node pushing here and then leave lower_block signature unchanged.

frame_id,
loop_scope: Some(LoopScope {
loop_expr,
iteration_count,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: this is very "code golf" of me, but the extra local to define iteration_count feels unnecessary when this line could just be interation_count: 0, instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants