Skip to content

Fix #48: reject non-integer measurement targets with InvalidCircuitException - #97

Open
corgab wants to merge 2 commits into
mainfrom
fix/48-measure-target-types
Open

Fix #48: reject non-integer measurement targets with InvalidCircuitException#97
corgab wants to merge 2 commits into
mainfrom
fix/48-measure-target-types

Conversation

@corgab

@corgab corgab commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Gate::measure(int|array|null $targets) stored any array verbatim, so measure(['a']) passed the parameter type and failed later in CircuitBuilder::validateTargets(string $gate, int ...$qubits) as a raw TypeError, unlike every other circuit-shape mistake, which throws InvalidCircuitException. Worse, Gate::fromArray() decoded serialized values with (int): a corrupted queued definition carrying targets: ["a"] was silently rebuilt as a measurement of qubit 0, and the same cast turned target: "a" on an h gate into qubit 0 too.

Plan

Validate every qubit index once, in Gate, for both entry points: the fluent measure() call and the fromArray() decoding path, for measurements and for every other gate's qubit keys.

Changes

  • Gate::integerIndex(string $gate, mixed $value): int (private): the value must be an int, otherwise InvalidCircuitException::invalidQubitIndex(GATE, $value). integerIndices() applies it to a list and reindexes it.
  • Gate::measure() uses it for the array form. Gate::fromArray() uses it for every qubit key instead of (int), and decodeMeasureTargets() returns null only for a null targets, rejects a scalar (previously read as "measure all") and validates the elements; the empty-array rule stays in measure(), which every path reaches.
  • InvalidCircuitException::invalidQubitIndex(string $gate, mixed $value): names the gate and the offending value (Gate H expects integer qubit indices, got 'a').
  • README circuit section: one sentence on integer indices and on what happens to a corrupted queued definition.

No config change, no new dependencies, no change for valid integer indices (a keyed measure() array such as [2 => 1, 5 => 0] is now reindexed to [1, 0], which is what the Python side already expected; a keyed array previously serialized as a JSON object).

Tests

  • GateTest: dataset of invalid measure() targets (string, numeric string, float, mixed with a valid index, nested array); explicit targets are reindexed; fromArray() dataset covering a string measure target, a scalar targets, a string h target, a numeric-string h target and a float cnot control, all throwing with the value in the message.
  • CircuitBuilderTest: ->measure(['a']) throws InvalidCircuitException, not a TypeError.
  • The datasets fail on main (TypeError, silent qubit 0, or silent measure-all).

Local: pint passed, pest 804 tests passed.

Closes #48

Gate::measure() stored whatever array it was given, so a string or float
target reached the int-typed range check as a raw TypeError, unlike every
other circuit-shape mistake, and Gate::fromArray() cast such values with
(int), silently turning a corrupted queued definition into a measurement
of qubit 0. Both paths now validate each element and throw
InvalidCircuitException::invalidMeasurementTarget() naming the offending
value; explicit targets are reindexed, and an empty targets array in a
serialized definition is rejected like it is in measure().

Closes #48
…e targets

The (int) cast the previous commit removed for measure targets was still
applied to the qubit keys of every other gate rebuilt from a serialized
definition, so a corrupted payload could turn "a" into qubit 0 for h or
cnot just as it could for measure. One integerIndex() helper now guards
both paths through InvalidCircuitException::invalidQubitIndex(), a scalar
targets value in a measure definition is rejected instead of silently
meaning "measure all", and the redundant empty-targets guard is gone
because measure() already owns that rule.
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.

[circuit] Gate::measure() array element types are unchecked, causing a TypeError instead of InvalidCircuitException

1 participant