Measurement-dependent classical errors (division by zero, array index) compile to unchecked instructions with `ret i64 0` — simulator raises, compiled output continues
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 1k
- Forks
- 212
- Avg merge
- 3d 8h
- Merged PRs (30d)
- 65
Description
## Summary
`qsc` catches integer division by zero and out-of-range array indices at compile time when the operands are static. When the operand depends on a measurement result, the Adaptive-family profiles emit the unchecked instruction — no diagnostic, no runtime check, and no path to a non-zero exit code. The same program raises under simulation and silently continues when compiled. This is not a conformance claim (the spec contemplates such errors as UB); it is that one source means two different things depending on `targetProfile`, and that the emitted IR has no instruction that could report the failure at all.
## Repro A — division by zero (`Adaptive_RI`, `Adaptive_RIF`, `Adaptive`)
```qsharp
namespace T {
@EntryPoint()
operation Main() : Int {
use q = Qubit();
X(q); // deterministic: M(q) is always One
let d = M(q) == One ? 0 | 1; // divisor is always 0
return 100 / d;
}
}
```
`Unrestricted` simulator: raises `Error: division by zero`, 20/20 shots. All three Adaptive-family profiles compile clean, with **zero comparison (`icmp`) instructions** in the module and an unchecked `sdiv` whose divisor is always 0, ending in `ret i64 0`. On `Adaptive_RI` / `Adaptive_RIF` (QIR v1):
```llvm
block_3:
%var_8 = phi i64 [0, %block_1], [1, %block_2]
%var_6 = sdiv i64 100, %var_8 ; %var_8 is 0 on every shot
call void @__quantum__rt__int_record_output(i64 %var_6, i8* ...)
ret i64 0
```
Plain `Adaptive` (QIR v2) lowers the divisor via `alloca`/`load` rather than a `phi`, but is otherwise the same — unchecked `sdiv i64 100, `, zero `icmp`, `ret i64 0`. A *static* divisor (`100 / 0`) is caught on all three: `Qsc.PartialEval.EvaluationFailed: division by zero`. `X(q)` is used so the divisor is deterministically 0 and no sampling argument is needed; `H(q)` makes it probabilistic with the same result.
## Repro B — array index (`Adaptive` only)
The docs promise this one unconditionally ([item access](https://learn.microsoft.com/en-us/azure/quantum/user-guide/language/expressions/itemaccessexpressions)): *"An exception is thrown at runtime if the index … is outside the bounds of the array."* The simulator does; `Adaptive` does not.
```qsharp
namespace T {
@EntryPoint()
operation Main() : Int {
let table = [100, 200, 300];
use q = Qubit[2];
H(q[0]); H(q[1]);
let a = M(q[0]) == One ? 1 | 0;
let b = M(q[1]) == One ? 2 | 0;
return table[a + b]; // index in {0,1,2,3}; 3 is out of bounds
}
}
```
```llvm
@array0 = internal constant [3 x i64] [i64 100, i64 200, i64 300]
%var_12 = getelementptr i64, ptr @array0, i64 %var_11
%var_20 = load i64, ptr %var_12
```
`Adaptive_RI`/`Adaptive_RIF` reject this with `Qsc.CapabilitiesCk.UseOfDynamicIndex`. `Adaptive` permits dynamic indices by design (module flags `!"arrays" i1 true`, `!"backwards_branching" i2 3`) — that is intended; the gap is that permitting them comes with no check. A static out-of-range index is caught on all three. (SSA names above are illustrative and depend on the exact source.)
## Why these are the same bug
Both follow the same shape in `qsc_partial_eval`: reject only the statically-provable case, emit unchecked otherwise.
`lib.rs:3213` (comment at 3212) — literal only:
```rust
// Validate that the RHS is not a zero.
if let Operand::Literal(Literal::Integer(0)) = rhs_operand {
let error = EvalError::DivZero(bin_op_expr_span).into();
return Err(error);
}
```
`lib.rs:5094` — only when the array is empty:
```rust
if array.is_empty() {
// Even though we don't know what the index value is, we know any index into an empty array is out of range,
// so just return an error with index 0 here.
```
`lib.rs:507` — the exit code is a hardcoded literal:
```rust
// Encode finalize as `Return(Some(Integer(0)))` so the QIR v2 renderer emits
// the entry-point convention as `ret i64 0` through the value-return path.
```
And `qsc_rir`'s `Instruction` enum (`rir.rs:377`) has no `Fail`/`Abort`/`Trap` variant — so a compiled program has no instruction that could report a runtime failure, whatever happens. The division and array cases are two symptoms of that one structural gap.
## Expected
Any one of these, cheapest first:
1. A compile-time diagnostic when a measurement-dependent value reaches a fallible classical operation — the RCA machinery already does exactly this for `UseOfDynamicIndex`.
2. An emitted runtime check producing a non-zero exit code. The spec provides the mechanism and assigns it to the compiler: *"Injecting suitable logic to produce a non-zero exit code … is generally up to the compiler, and the backend is not required to detect runtime failures"*; *"The program IR must use exit codes within the range `1` to `63` to indicate a failure."*
3. At minimum, publish the limitation. The v1.30.0 notes point to the wiki QIR page for the Adaptive profile's "latest capabilities, limitations, and known issues", but that page has only a Runtime Capabilities Check error reference — no limitations section. Repro A is not specific to the in-progress `Adaptive` profile: it reproduces on `Adaptive_RI`/`Adaptive_RIF` too, and the literal-only Div check dates to #1499 (2024-05-13). Unchanged on `main` as of `ebe6090`.
## Not claiming a conformance violation
The spec contemplates real-time classical errors as UB — *"An Adaptive Profile program that undergoes a real-time classical error (for example unchecked division by zero) has undefined behavior."* The point is narrower: the same Q# source silently means two different things depending on `targetProfile`, the compiled path drops a check the simulator performs, and the IR cannot express the failure — and none of this is documented.
## Environment
`qdk` 1.30.0 / `qsharp` 1.30.0 (unchanged on `main` as of `ebe6090`), Python 3.12, Windows
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in qsc_partial_eval/lib.rs around lines 3213 and 5094, then inspect the hardcoded return at line 507 and qsc_rir/rir.rs around the Instruction enum at line 377. Reproduce the measurement-dependent division and array-index cases from the issue, then determine and implement an agreed failure behavior—diagnostic, runtime failure, or documented limitation—with coverage for the affected Adaptive profiles.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100