Case Study 2: What the Compiler Caught, and What It Did Not

The premise

Static typing is easy to argue about in the abstract and easy to settle in the concrete. So: take the bugs this book has actually documented — real failures, each of which cost real time in Parts I and II — and ask, one at a time, would the Q# compiler have caught this?

No hypotheticals. Every entry is a bug that appeared in an earlier chapter.

The scorecard

# Bug Chapter Q# catches it?
1 dump takes a stream, load takes a filename 6 compile error
2 Layout trap: observable not mapped with apply_layout 7 no
3 Parameter ordering: theta1…theta12 sorts lexicographically 8 cannot occur
4 QASM round-trip loses global phase 6 no
5 Dead qubit chosen by the layout 4, 12 no
6 Readout mitigation applied to a gate-error-dominated result 13 no
7 DD enabled but placed where there was no idle time 13 no
8 Cirq/Qiskit endianness mismatch 14 no
9 Test case optimized away before it could fail 11 no
10 Qubit reused without reset, mid-operation 9 no
11 Qubit released to the pool in $\lvert 1\rangle$ runtime error
12 Operation used in reverse without an inverse defined 13 compile error

Three caught, one made impossible, eight missed.

That is the honest number, and it is worth going through both columns carefully because the shape of each column is more informative than the tally.

What it caught

Bug 1: argument type confusion

Chapter 6 §6.4 documented that qasm3.dump takes a file stream while qasm3.load takes a filename — an asymmetry discovered by the code failing at runtime.

In Q#, passing a filename where a stream is expected is:

  Qdk.Qsc.TypeCk.TyMismatch
    x type error
    `-> expected Double, found Qubit

(the message shown is from the analogous Rx(q, 1.0) test, where the argument order is swapped.)

This is the class static typing is genuinely good at: not deep logic errors, but interface errors. What goes in which slot.

Bug 12: missing inverse

Chapter 13 §13.5's gate folding needs $U^\dagger$. In Python you call .inverse() and find out at runtime whether it works — which, for an ISA circuit, produced IBMInputValueError: The instruction sxdg ... is not supported, discovered by the primitive rejecting the job.

In Q#, using an operation in reverse without declaring it reversible does not compile:

  Qdk.Qsc.TypeCk.MissingFunctor
    x type error
    `-> expected superset of Adj, found empty set

And the declaration is checked, not merely recorded. An operation containing a measurement cannot be declared is Adj:

  Qdk.Qsc.LogicSeparation.OpCallForbidden

So is Adj is a proof obligation the compiler discharges: you assert the body is reversible, and it verifies the assertion. That is meaningfully stronger than a type annotation.

Bug 3: the one that cannot occur

Chapter 8's most expensive bug: twelve parameters named theta1 … theta12, bound by position against a lexicographically sorted parameter list, putting eleven of twelve values in the wrong gate and producing a plausible-looking wrong answer.

Q# has no such API. Parameters are function arguments with declared types, bound by position against a declared signature the compiler checks, or by name. There is no sorted-list-of-names step for an ordering to be silently wrong in.

This is the strongest form of the argument. Not "the compiler catches it" but "the language does not contain the shape of the mistake." Chapter 14 §14.8 found the same thing about Cirq's dict-based ParamResolver. Two frameworks eliminated a bug that cost Qiskit users real debugging time, by making a different API choice.

What it missed, and why the pattern matters

Look at bugs 2, 4, 5, 6, 7, 8, 9, and 10 together. They have something in common.

Every one of them is a well-typed program that does the wrong thing.

  • Bug 5 (dead qubit): submitting to physical qubit 84 is type-correct. The qubit exists. It is simply broken, and no type system knows that.
  • Bug 8 (endianness): a mistranslation between conventions is perfectly well-typed on both sides — that is exactly why Chapter 14's four verification tests passed.
  • Bug 7 (inert DD): the pass ran, returned a valid circuit, and inserted four gates. Everything type-checked. It just did not do anything.
  • Bug 2 (layout trap): apply_layout returns a valid observable whether or not you called it. The version without it is well-typed and silently returns 0.8838 instead of 1.8135.
  • Bug 10 (mid-operation reuse): §15.5 measured this running cleanly in Q# — the qubit is reset before scope exit, so the discipline is satisfied while the algorithm is still wrong.

The boundary is sharp:

A type system catches errors of form. Almost every bug in this book was an error of correspondence — between the program and the machine, between two conventions, or between what a technique does and what the situation needs.

Types constrain what you can write. They say nothing about whether what you wrote matches reality, because they have no access to reality — no calibration data, no knowledge of which qubit is broken today, no model of what your oracle is supposed to mark.

Reading the scorecard honestly

Eight of twelve missed is not a criticism of Q#. It is a measurement of what the technique can reach, and the same table would look worse for every other framework here: the three caught and one impossible would drop to one impossible for Qiskit, since the other three are exactly the ones the type system handles.

The right comparison is not "Q# catches most bugs" — it does not — but "Q# catches a class of bug for free, permanently, on every build." Interface errors do not require a test to be written, do not depend on a code path being exercised, and do not appear after forty minutes in a queue. That is a real and durable benefit with a real cost in ceremony.

And there is a second-order effect worth noting: the bugs Q# catches are the cheap ones to catch by other means. Argument-order confusion usually surfaces in the first test run. The bugs that cost this book the most time — Chapter 12's stuck qubit, Chapter 13's inert DD, Chapter 14's endianness — were expensive precisely because they ran successfully and returned plausible numbers, and no type system addresses that.

What addresses it is the practice this book has been building instead: the noiseless reference (Ch. 7), the two-axis noise signature (Ch. 11), the preflight check (Ch. 12), the decision procedure (Ch. 12 §12.7), the asymmetric test case (Ch. 14). Those catch errors of correspondence, and they are framework-independent.

The lessons

Static typing catches errors of form, cheaply and permanently. Argument order, type confusion, undeclared capabilities, mutation of constants. Worth having. Not worth overselling.

The expensive bugs are errors of correspondence, and no compiler reaches them. A program can be perfectly well-typed and perfectly wrong about the world. Every technique in Part II exists because of this.

"The language does not contain the shape of the mistake" beats "the compiler catches the mistake." Chapter 8's parameter bug is impossible in Q# and in Cirq, not because they check for it, but because neither offers a positional binding against a sorted name list. API design prevents more bugs than type checking does.

A guarantee is only as broad as its statement — §15.5's release check secures the qubit pool, not your algorithm; and this book's fourth instance of that lesson.

And judge the trade the way you would anywhere else. Static typing on a fifty-line script is ceremony. On a codebase that will live for years and be modified by people who did not write it, the free-and-permanent part starts to matter a great deal. That is the same calculation as every other language you have made this decision about; quantum does not change it.


Reproduce it: code/example-02-type-system.py and code/example-04-functors.py run every compiler rejection in this case study; code/example-03-qubit-lifetime.py demonstrates bug 10 running cleanly.