Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/42.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The `.qp` format version now follows the library version, truncated to `major.minor`, so this release writes `#!QProgram 0.2` instead of the fixed `1.0`. `FORMAT_VERSION` is now derived rather than a literal: `library_major_minor` in the new `qprogram/_version.py` reads the installed distribution's version through `importlib.metadata`, and imports nothing else from the package, so `qprogram/serialization/_format.py` stays the leaf module the writer and the parser both import. The compatibility check is unchanged — the parser accepts any minor within its own major and rejects a different one — so a file written by an earlier release no longer loads: `#!QProgram 1.0` now fails with `Line 1: Unsupported format version 1.0`. The `.wfl` waveform library format follows the library version through the same helper, so a `WaveformLibrary` now writes `#!WaveformLibrary 0.2` and refuses a `1.0` file with `Line 1: Unsupported WaveformLibrary format version 1.0`.
3 changes: 2 additions & 1 deletion docs/developer/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ qprogram/
├── errors.py # exception hierarchy
├── _reserved.py # RESERVED_KEYWORDS
├── _structural.py # ast_eq / ast_hash helpers
├── _version.py # the library version, cut to major.minor for the headers
├── vendor.py # VendorNamespace base
├── platform.py # PlatformProtocol: capabilities, validate, plan, explain, execute
├── protocol.py # capability descriptors, Diagnostic, Profile, token registry
Expand All @@ -60,7 +61,7 @@ qprogram/
├── parser.py # loads / load
├── registry.py # registry-driven dispatch
├── _specs.py # per-op serialize/parse callbacks
└── _format.py # the format version constant
└── _format.py # the .qp format version constant
```

`qprogram` is the whole language: the AST, the `.qp` format, the capability
Expand Down
23 changes: 16 additions & 7 deletions docs/developer/serialization-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,26 @@ source and keep their intra-package imports, since `import qprogram` from
inside the package would close an import cycle. Anything written against the
installed package uses `import qprogram as qp`.

The format version is one constant, shared by both directions:
The format version is one constant, shared by both directions, and it follows
the library version truncated to `major.minor`:

```python
# src/qprogram/serialization/_format.py
FORMAT_VERSION: Final[str] = "1.0"
FORMAT_VERSION: Final[str] = library_major_minor()
```

It is emitted in the `#!QProgram` header and checked on load. Only the major
component is binding: a file whose major differs is rejected with
`library_major_minor` lives in `src/qprogram/_version.py` and reads the
installed distribution version through `importlib.metadata`, not
`qprogram.__version__`, so `_format` stays a leaf importing one stdlib-only
module and nothing else in the package. A source tree with no installed
metadata falls back to `"0.0"`. The `.wfl` format's
`WAVEFORM_LIBRARY_FORMAT_VERSION` is derived the same way, which is why the two
headers carry the same number.

The version is emitted in the `#!QProgram` header and checked on load. Only the
major component is binding: a file whose major differs is rejected with
`Unsupported format version`, and any minor within the same major loads, so a
`1.4` file opens under a `1.0` runtime.
`0.4` file opens under a `0.2` runtime.

## The registries

Expand Down Expand Up @@ -242,7 +251,7 @@ Put together, a program with metadata, a schema, a fragment, an `average`, and
a sweep writes as:

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "ordering demo"
Expand Down Expand Up @@ -483,7 +492,7 @@ For a sweep whose values are large or live outside the program, use the file
source instead. The path, not the data, is what the `.qp` file carries:

```
#!QProgram 1.0
#!QProgram 0.2

body:
var amp
Expand Down
2 changes: 1 addition & 1 deletion docs/developer/vendor-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ in the program but no version is registered.`
A complete file for a two-operation program looks like this:

```
#!QProgram 1.0
#!QProgram 0.2

require fake_inst 0.1

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/active-reset.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ from.
## What it produces

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "active_reset"
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/cpmg-fragments.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ free to run them at the same time.
## What it produces

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "cpmg"
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/cz-chevron.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ per-bus counter, so both come out as `m0` under different bus prefixes:
## What it looks like on disk

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "cz_chevron"
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/multiplexed-readout.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ number. Four measurements on four buses are all `m0`; three on one bus would be
## What it produces

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "multiplexed_rabi"
Expand Down Expand Up @@ -187,7 +187,7 @@ library.save("chip.wfl")
```

```
#!WaveformLibrary 1.0
#!WaveformLibrary 0.2
"readout" q[0].readout = IQPair(I=Square(amplitude=0.9, duration=1000), Q=Square(amplitude=0.0, duration=1000))
"readout" q[2].readout = IQPair(I=Square(amplitude=0.7, duration=3000), Q=Square(amplitude=0.0, duration=3000))
"readout" q[*].readout = IQPair(I=Square(amplitude=0.5, duration=2000), Q=Square(amplitude=0.0, duration=2000))
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/qubit-spectroscopy.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Both are the same decisions the [Rabi example](rabi.md) explains at length.
## What it produces

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "qubit_spectroscopy"
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/rabi.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ after a `.qp` round-trip still finds the right record.
`qp.dumps(program)` returns this, and it is the whole file:

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "rabi"
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/resonator-spectroscopy.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ coarse enough to find the dip in one pass. The fine scan comes after.
## What the platform makes of it

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "resonator_spectroscopy"
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/single-shot-readout.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ transposes the result from `(2, 2000)` to `(2000, 2)` rather than being free.
## What it produces

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "single_shot_readout"
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/t1-and-ramsey.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ times so that the tail is flat enough to fit a baseline against.
## What it produces

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "t1"
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ that is how you address the measurement's data after a run.
No platform is involved yet. The output is the program in `.qp` form:

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "rabi"
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/control-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ print(qp.dumps(program))
```

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "control-flow-forms"
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/fragments.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ Fragment definitions are top-level sections before `body:`, and a call site is
a bare `name(args)` statement. `qp.dumps(p)` on the program above gives:

```
#!QProgram 1.0
#!QProgram 0.2

fragment x_pulse(drive, amp):
play drive Gaussian(amplitude=amp, duration=40, sigma=8)
Expand Down Expand Up @@ -283,7 +283,7 @@ accepts, is in [the `.qp` format reference](../reference/qp-format.md#fragments)
untouched. On the program above:

```
#!QProgram 1.0
#!QProgram 0.2

body:
var g
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/measurements.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ The writer always emits the measurement name as a `name=` keyword, so nothing is
inferred on the way back in:

```
#!QProgram 1.0
#!QProgram 0.2

schema:
element q:
Expand Down
19 changes: 11 additions & 8 deletions docs/guide/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ This is `qp.dumps` output for a T1 experiment built on
`qp.BusSchema.transmon()`, with an averaging block around a delay sweep:

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "t1"
Expand Down Expand Up @@ -167,25 +167,28 @@ the handle is allocated before the path is promoted to a `BusRef`.

Every file opens with the format version, which the writer takes from
`FORMAT_VERSION` in `qprogram/serialization/_format.py`, the single constant
both sides read:
both sides read. It is the installed library version truncated to
`major.minor`, so a `qprogram` 0.2.1 writes:

```
#!QProgram 1.0
#!QProgram 0.2
```

Only the major component is binding. The parser checks the header before
anything else and rejects a different major, so `#!QProgram 2.0` fails with
`Line 1: Unsupported format version 2.0` while `#!QProgram 1.7` loads on
anything else and rejects a different major, so `#!QProgram 1.0` fails with
`Line 1: Unsupported format version 1.0` while `#!QProgram 0.7` loads on
today's parser, which reads it with the features it knows. That is the
compatibility contract: minor versions add sections, operations, and
constructs without breaking older readers, and a major bump is reserved for a
change that does.
change that does. Since the version comes from the library, a release that
leaves the format alone still moves the minor, and the library's own 1.0 is
where files written by an 0.x release stop loading.

A program that uses vendor operations or vendor blocks carries one `require`
line per vendor, directly after the header:

```
#!QProgram 1.0
#!QProgram 0.2

require myvendor 0.1

Expand Down Expand Up @@ -464,7 +467,7 @@ library = qp.WaveformLibrary.loads(text)
exactly. This is the library built above:

```
#!WaveformLibrary 1.0
#!WaveformLibrary 0.2
"pi_pulse" q[0].drive = IQDrag(amplitude=0.5, duration=40, sigma=8, beta=0.1)
"pi_pulse" q[1].drive = IQDrag(amplitude=0.9, duration=40, sigma=8, beta=0.1)
"cz" c[0,1].flux = Square(amplitude=0.3, duration=200)
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ A program declaring one annotated variable and one bare one serializes like
this, and the file round-trips back to a program equal to the original:

```
#!QProgram 1.0
#!QProgram 0.2

metadata:
label: "ramsey"
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/waveforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ arrays are written in full, because the parser has no way to recover dropped
samples.

```
#!QProgram 1.0
#!QProgram 0.2

body:
play "drive_q0" IQDrag(amplitude=0.5, duration=40, sigma=8, beta=0.1)
Expand Down
14 changes: 7 additions & 7 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,13 @@ pattern.
## Versions and compatibility

The package is pre-1.0, so the Python API can change between releases without a
deprecation cycle. The `.qp` format carries its own version and is at `1.0`,
where only the major component is binding: the writer emits `#!QProgram 1.0`, a
`1.1` file still loads on this parser, and a `2.0` file raises `ParseError` with
`Unsupported format version 2.0`. Accepting a newer minor is deliberate, and the
cost is that a file using grammar this parser does not know fails somewhere in
its body instead of at the header. A file with no header at all fails
immediately with `Missing #!QProgram header`.
deprecation cycle. The `.qp` format version follows the library version
truncated to `major.minor`, so this release writes `#!QProgram 0.2`. Only the
major component is binding: a `0.7` file still loads on this parser, and a `1.0`
file raises `ParseError` with `Unsupported format version 1.0`. Accepting a
newer minor is deliberate, and the cost is that a file using grammar this parser
does not know fails somewhere in its body instead of at the header. A file with
no header at all fails immediately with `Missing #!QProgram header`.

Vendor compatibility is checked one `require` line at a time, before any of the
body is built, so a rejected file leaves no partially loaded program: the majors
Expand Down
12 changes: 6 additions & 6 deletions docs/reference/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,11 @@ information:
```python
import qprogram as qp

qp.loads("#!QProgram 1.0\n\nbody:\n var 1x\n")
qp.loads("#!QProgram 0.2\n\nbody:\n var 1x\n")
# ParseError: Line 4: variable id '1x' is invalid: must match
# [A-Za-z_][A-Za-z0-9_]* (no spaces or special characters)

qp.loads("#!QProgram 1.0\n\nbody:\n var if\n")
qp.loads("#!QProgram 0.2\n\nbody:\n var if\n")
# InvalidVariableIdError: Variable id 'if' is reserved for future QProgram
# syntax ...
```
Expand All @@ -305,7 +305,7 @@ class's own `TypeError`:
```python
import qprogram as qp

qp.loads('#!QProgram 1.0\n\nbody:\n play "b" Gaussian(amplitude=0.5)\n')
qp.loads('#!QProgram 0.2\n\nbody:\n play "b" Gaussian(amplitude=0.5)\n')
# TypeError: Gaussian.__init__() missing 2 required positional arguments:
# 'duration' and 'sigma'
```
Expand All @@ -332,17 +332,17 @@ almost identical mistakes read differently:
```python
import qprogram as qp

qp.loads('#!QProgram 1.0\n\nbody:\n play "b" Bogus(amplitude=0.5)\n')
qp.loads('#!QProgram 0.2\n\nbody:\n play "b" Bogus(amplitude=0.5)\n')
# ParseError: Unknown waveform or sweep source type: Bogus
# ... with line_num == 0, even though the offending line is line 4

qp.loads("#!QProgram 1.0\n\nbody:\n var x\n for x in Bogus(start=1):\n sync\n")
qp.loads("#!QProgram 0.2\n\nbody:\n var x\n for x in Bogus(start=1):\n sync\n")
# ParseError: Line 5: unknown sweep source 'Bogus'; registered sources are
# ['Concat', 'File', 'Linspace', 'Logspace', 'Range', 'Repeat', 'Rotate',
# 'Values']
# ... with line_num == 5

qp.loads('#!QProgram 1.0\n\nbody:\n var x\n set_phase "b" ("a" + x)\n')
qp.loads('#!QProgram 0.2\n\nbody:\n var x\n set_phase "b" ("a" + x)\n')
# ParseError: cannot use 'a' (_QuotedStr) as an expression operand
# ... with line_num == 0
```
Expand Down
7 changes: 4 additions & 3 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ source wins, and the page is a bug.

| Page | What it fixes |
|---|---|
| [.qp file format](qp-format.md) | Every production of format version 1.0: sections, the eleven core operation keywords, inline waveform constructors, sweep sources, expressions, the grammar summary, and what the parser rejects with which message; then the `.wfl` waveform library format, which is the other file the package reads. |
| [.qp file format](qp-format.md) | Every production of format version 0.2: sections, the eleven core operation keywords, inline waveform constructors, sweep sources, expressions, the grammar summary, and what the parser rejects with which message; then the `.wfl` waveform library format, which is the other file the package reads. |
| [Reserved keywords](reserved.md) | The 29 names in `qp.RESERVED_KEYWORDS`, which construction sites check them, and the wider rule that applies to vendor namespaces. |
| [Errors](errors.md) | The `QProgramError` hierarchy, which call raises which, and the two families of argument error that stay outside it as a plain `TypeError`. |
| [API reference](api-qprogram.md) | Signatures and docstrings for the names in `qprogram.__all__`, plus the submodule classes and extension points the guides name, rendered from `src/` by mkdocstrings. |

Three of the four are checkable against the package at runtime. The format
version comes from `qprogram.serialization._format.FORMAT_VERSION`, currently
`"1.0"`, and is what the writer emits in the `#!QProgram` header and what the
parser compares a file's major version against. The canonical grammar ships as
`"0.2"` since it follows the library version truncated to `major.minor`, and is
what the writer emits in the `#!QProgram` header and what the parser compares a
file's major version against. The canonical grammar ships as
`src/qprogram/grammar/qp.lark` and is readable with
`qprogram.grammar.grammar_text()`. The reserved set is `qp.RESERVED_KEYWORDS`.
The API page is generated from one mkdocstrings directive per symbol, so a new
Expand Down
Loading
Loading