Skip to content

Accept controlled(par) @(control, target) wire counts - #528

Merged
cursor[bot] merged 1 commit into
mainfrom
cursor/controlled-par-target-count-6d39
Oct 1, 2026
Merged

cursor[bot] merged 1 commit into
mainfrom
cursor/controlled-par-target-count-6d39

Conversation

@arniber21

@arniber21 arniber21 commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

controlled(par { … }) @(control, (t1, t2)) names one control wire plus the body's wires, which is the controlled circuit's width. The typechecker counted the outer pair as two flat targets and rejected the placement with GateTargetArity, even though decompose_controlled already distributes control over par arms.

This counts those wires instead of the outer slots:

  • @(control, (t1, …, tk)) and the flat @(control, t1, …, tk) must name k + 1 qubits.
  • controlled(par …) @(control, start) means contiguous body wires beginning at start, and start must be a non-negative integer literal. A symbolic start is rejected, so every implied index is compared to the register width. Elaboration fails closed if a non-literal start would have to be shifted.
  • Each par arm takes a slice of that width. The offset is the body's width, not the controlled step's max index (that index includes the control wire).

controlled(H) @(0, 1) is unchanged. A non-par body wider than one qubit still has to name every wire; a bare start index is rejected.

Width-1 arms lower to quantum.circ. controlled(par { CNOT @(0, 1) }) @(0, (1, 2)) typechecks and then fails elaboration of the multi-qubit arm. The support matrix keeps this row partial: there is no test/lit OpenQASM fixture and no neutral-atom fixture.

Refs #369.

Open in Web Open in Cursor 

@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
quon Ready Ready Preview Oct 1, 2026 6:06am UTC

Comment thread frontend/tests/lower.rs Outdated
"expected a 3-qubit controlled par: {text}"
);
assert!(
text.contains(r#"gate_name = "CNOT""#),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is the issue's surface program, but the assertions do not lock the tuple. Any lowering that emits one CNOT inside a 3-qubit ctrl_layer passes, including both arms on (0, 1) or a dropped second arm.

controlled_parn_explicit_target_tuple does check pairs (0, 1) and (0, 2), but it calls decompose_controlled with an already-split target. It never parses @(0, (1, 2)) and never runs split_control_target. Please assert the CNOT operand pairs produced from this surface spelling (and from @(0, 1, 2) / @(ctrl, start) if those are part of the contract).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

frontend/tests/lower.rs::controlled_par_nested_targets_lower now parses and lowers the surface programs, and asserts the CNOT operand pairs rather than a bare gate_name = "CNOT":

  • controlled(par { H @0, H @0 }) @(0, (1, 2))
  • controlled(par { H @0, H @0 }) @(0, 1, 2)
  • controlled(par { H @0 } * 2) @(0, 1)

Each must emit CNOT pairs (0, 1) then (0, 2). The pairs come from tracking each quantum.circ.gate SSA value back to its block argument, so both arms landing on (0, 1), or a dropped second arm, fails the test.

The elaborator tests build the same nested and flat GateApp values and run them through elaborate_circuit_body, which calls split_control_target, then assert those same CNOT operand pairs. controlled_parn_explicit_target_tuple still calls decompose_controlled with an already-split target; it is no longer the only coverage of this spelling.

Comment thread frontend/src/typecheck/circuit.rs Outdated
// contiguously. The two written indices are control and that start.
let par_body = matches!(inner.0, Expr::Par(_, _) | Expr::ParN(_));
if par_body && body_is_single_index && items.len() == 2 && arity > 2 && indices.len() == 2 {
let implied_last = literal_index(indices[1]).map(|start| start.saturating_add(arity - 2));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The contiguous-start shorthand typechecks when the start is not a literal (implied_last stays None), but elaboration does not occupy start .. start+(arity-2).

wire_slice shifts that start through shift_qubit_targets, which only rewrites Expr::Int and clones any other expression. controlled(par { H @0 } * 2) @(0, s) therefore places both arms on s. This spelling used to be GateTargetArity (2 slots vs width 3), so the PR newly accepts it while the wires still do not match the typechecker.

Reject a non-literal start here, or build start+i and fail closed when it does not evaluate to a literal. Bounds checking has the same hole: with implied_last == None, s+1 is never compared to the register width.

Requesting changes. I am not approving.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A non-literal start is now a type error (NonLiteralControlledStart). The contiguous shorthand only succeeds when start is a non-negative integer literal, so implied_last is always Some(start + arity - 2) and the existing register-cap check compares that index to the width. controlled(par { H @0 } * 2) @(0, 2) inside Circuit<3, …> is IndexOutOfBounds { index: 3, width: 3 }: the written start is in range, and wire 3 is not.

wire_slice fails closed when a non-Expr::Int start would be shifted or expanded. shift_qubit_targets still only rewrites integer literals, and that path is no longer reachable for a symbolic @(control, start). fn f(s: Int) … @(0, s) is rejected in the typechecker, and decompose_controlled on a Var start returns an elaboration error instead of placing both arms on s.

| `repeat(k, c)` (concrete & symbolic count) | ✓ | ✓ | ✓ | ✓ | — | Grover/QFT fixtures |
| `controlled(named_gate)` / `controlled(Rz(θ))` | ✓ | ✓ | ✓ | ✓ | — | Distributed via `decompose_controlled` |
| `controlled(par { … })` / `controlled(par { c } * k)` | ✓ | ✗ | ✗ | ✗ | — | Typechecker rejects multi-target tuple; see [#369](#known-limitations) |
| `controlled(par { … })` / `controlled(par { c } * k)` | ✓ | ✓ | ✓ | ✓ | — | `@(ctrl, (t₁, …))`; `par` repeat also accepts contiguous `@(ctrl, start)` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This page defines ✓ as stable and covered by a CI fixture, and QASM/NA ✓ specifically as test/lit FileCheck. This PR adds no lit or neutral-atom fixture; the only pipeline test is the MLIR substring check in lower.rs.

A multi-qubit arm still does not lower. controlled(par { CNOT @(0, 1) }) @(0, (1, 2)) typechecks (controlled width 3, three named wires) and then fails in the GateApp arm of decompose_controlled (controlled() of a multi-qubit gate in a named circuit body). controlled_named_gate rejects the same shape with controlled() of a multi-qubit gate. The row should stay partial until that is supported or explicitly excluded.

"Fixes #369" plus deleting the known-limitation note will close the issue while QASM and NA are still unchecked and that arm still fails after typecheck.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The closing keyword is gone from the title, the body, and the branch commit. This does not close #369.

The support-matrix row is partial again. Typecheck, QASM, and NA are ◐. QASM and NA are not marked stable: there is no test/lit fixture and no neutral-atom fixture. The known-limitation note is restored.

controlled(par { CNOT @(0, 1) }) @(0, (1, 2)) still typechecks (controlled_par_multiqubit_arm_typechecks) and then fails elaboration of the multi-qubit arm (controlled_par_multiqubit_arm_does_not_lower). That path stays partial until it lowers.

@cursor

cursor Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Requesting changes. Not approved.

For literal indices, controlled(par { H @0, H @0 }) @(0, (1, 2)) is counted as the control wire plus the body tuple, and width-1 arms are sliced onto those wires. I would not merge this yet.

  1. @(control, start) typechecks for a non-literal start (implied_last stays None) while shift_qubit_targets only rewrites Expr::Int, so both arms stay on s. See frontend/src/typecheck/circuit.rs.
  2. frontend/tests/lower.rs does not lock the surface tuple's CNOT operands. The elaborate test that checks (0, 1) and (0, 2) never parses @(0, (1, 2)).
  3. The support matrix marks QASM and NA as stable ✓, and the body says "Fixes controlled(par{...}) target-count mismatch between typechecker and surface @ syntax #369", with no lit/NA fixture. controlled(par { CNOT @(0, 1) }) @(0, (1, 2)) typechecks and then fails elaboration.

No new unwrap/expect in non-test library code. I will re-review after a push.

controlled(par { ... }) @(control, (t1, t2)) names k+1 wires, but the
typechecker counted the outer pair as two targets and rejected it. Count
the control wire plus the body's target tuple (and the flat and
contiguous-start spellings), and distribute those wires across par arms
when elaborating.

A contiguous start must be a non-negative literal so every implied index
is bounds-checked. Width-1 arms lower; a multi-qubit arm still fails
elaboration. Refs #369.

Co-authored-by: Arnab Ghosh <ag2583@cornell.edu>
@cursor cursor Bot changed the title fix: accept controlled(par) @(control, target) wire counts Accept controlled(par) @(control, target) wire counts Oct 1, 2026
@cursor
cursor Bot force-pushed the cursor/controlled-par-target-count-6d39 branch from 4d9f8c9 to 7b34086 Compare October 1, 2026 06:05
@cursor

cursor Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Approved. I would merge 7b340867.

The three earlier requests are met:

  1. Contiguous @(control, start) requires a non-negative integer literal (literal_index returns None for a negative or symbolic start, and the checker reports NonLiteralControlledStart). The implied last index is folded into the placement and compared with the circuit width cap: @(0, 2) on a width-3 register is IndexOutOfBounds { index: 3, width: 3 }. wire_slice returns an error instead of shifting a non-literal or negative start, so both arms are not left on that start. controlled_par_non_literal_start_fails_closed locks the elaboration failure.
  2. controlled_par_nested_targets_lower parses @(0, (1, 2)), @(0, 1, 2), and @(0, 1) through lower_program and requires the CNOT operand pairs (0, 1) then (0, 2).
  3. The PR body and commit say Refs #369, not a closing keyword. The support matrix keeps QASM and NA partial (◐), and the known-limitation note still says a multi-qubit arm typechecks and then fails elaboration. controlled_par_multiqubit_arm_does_not_lower covers that failure.

No new unwrap/expect in non-test library code. Not merging.

@arniber21
arniber21 marked this pull request as ready for review October 1, 2026 06:09
@cursor
cursor Bot merged commit bf51e03 into main Oct 1, 2026
21 of 22 checks passed

This branch was successfully deployed

1 active deployment
Preview — 7b340867 Deployed Oct 1, 2026 by vercel[bot]
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.

2 participants