Skip to content

Add the qdk.openqasm parser and semantic analysis Python API - #3580

Open
Ian Davis (idavis) wants to merge 5 commits into
mainfrom
iadavis/qdk-openqasm-python
Open

Add the qdk.openqasm parser and semantic analysis Python API#3580
Ian Davis (idavis) wants to merge 5 commits into
mainfrom
iadavis/qdk-openqasm-python

Conversation

@idavis

@idavis Ian Davis (idavis) commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Exposed the QDK's OpenQASM front end to Python. The qdk.openqasm package gained two new public modules, parser for the syntactic AST and semantic for the resolved, typed AST, plus a shared source-document layer, a read-only visitor, and canonical serialization. Both trees are eagerly materialized, read-only Python objects that hold no reference back into the Rust parser, so they can be retained, inspected across threads, and traversed after the call returns. The API is marked preview in its docstrings and will change soon as the OpenQASM spec is updated.

Node classes are not openqasm3.ast objects. Reference-parser code that mutates nodes or depends on openqasm3 types will not work unchanged. Class names follow the openqasm3 reference AST wherever an equivalent exists; variants with no equivalent take a descriptive QDK name.

Future work

To minimize the size of this PR, there is a set of work that is out of scope:

  • dumps to allow arbitrary nodes
  • dumps to allow semantic graphs
  • Mutating visitor
  • dumps to avoid reparse
  • ast.dump debug output
  • Follow proposed OpenQASM spec and non-normative guidelines for imports, versions, and projects
  • full separation of ast node types
  • Allow \ in OpenQASM strings

Changes

Python public API

Added three modules under qdk/openqasm/ and widened the package's __all__ to re-export them along with the classes both layers share.

  • parser.parse() returned a ParseResult carrying a Program, an immutable SourceDocument, and structured diagnostics. Errors were collected on the result rather than raised. parser.py exported 107 names, including parse_program, dumps/dump, QASMVisitor, QASM3ParsingError, QASMUnparseError, and the full syntactic node family.
  • semantic.analyze() returned an AnalysisResult adding a SymbolTable and resolved Type / const_value information. semantic.py exported 101 names covering the ~22-variant resolved type hierarchy, Angle, Duration, Symbol, and the semantic node family.
  • source.py exported the position and span machinery: Position, PositionEncoding, SourceRange, SourceDocument, SourceFile, SourceMap, and ResolutionStatus. Spans are global, half-open UTF-8 byte ranges mapped through document.source_map.range_from_span(span).
  • _layers.py added the SyntaxNode / SemanticNode abstract markers. Most class names appear in both layers, so a value named Program or IntType is otherwise ambiguous; the four shared base classes answer False to both.
  • _visitor.py added a QASMVisitor base that dispatches on concrete node type and recurses through each node's children(), with optional context-passing detected per method.
  • The include resolver accepted a mapping or a callback keyed on platform-neutral logical paths. stdgates.inc, qelib1.inc, and the QDK extension qdk.inc are built in; nothing else falls back to the filesystem or network, and unresolved keys become diagnostics. A fresh resolver bridge is used per call and the result retains neither the mapping nor the callback.

Native binding layer

Added the src/openqasm/ module tree to the qdk crate (~7,000 lines) and took a path dependency on qdk_openqasm.

  • node_macro.rs generates the per-variant #[pyclass] families declaratively; syntax.rs and semantic.rs declare the node sets, and nodes.rs, span.rs, source.rs, eq.rs, repr.rs, resolver.rs, and diagnostics.rs supply the supporting projections.
  • Registration consolidated into register_openqasm_submodule, called from _native. The pre-existing interop functions (run_qasm_program, compile_qasm_program_to_qir, compile_qasm_to_qsharp, compile_stim_to_qir, and friends) stayed on the flat _native module, only their registration site moved, so no import path changed for existing callers.
  • Semantic node classes register into a private _semantic native submodule so their un-prefixed names do not collide with the syntax layer's openqasm3-parity names in the flat module. Each class advertises qdk.openqasm.semantic as its __module__.
  • verify_classes_are_sendable() was extended through verify_interop_struct_traits() to cover the new classes.

Telemetry

Added six events to telemetry_events.py: on_parse_qasm / on_parse_qasm_end, on_analyze_qasm / on_analyze_qasm_end, and on_dumps_qasm / on_dumps_qasm_end. Properties are booleans and power-of-ten bucketed byte counts; no source text is emitted.

Tests

  • Added 17 test modules with roughly 234 test functions plus the qasm_corpus.py fixture helper, covering parsing, semantic analysis, resolved types, cast kinds, const values, structural equality and hashing, repr, source/position mapping, layer markers, node and stub docstrings, visitor dispatch, projection sharing, node reachability, unparsing, and the documentation examples.
  • Added three broadcast regression tests to test_qasm.py asserting that run, compile, and circuit (across all three CircuitGenerationMethod values) preserve operation multiplicity for qubit[3] q; x q;.

Documentation and benchmarks

  • qdk/_native.pyi grew by 2,044 lines of hand-maintained stubs for the new classes.
  • qdk_package/README.md gained an "OpenQASM parsing and analysis" section with subsections on navigating sources, symbols and diagnostics, resolved types and const values, comparison and hashing, include resolution, visiting, and canonical output.
  • vscode/ai/qdk-programming/python.md gained matching guidance for the agent-facing skill.
  • Added benchmarks/bench_openqasm_parsers.py, a self-provisioning script that compares qdk.openqasm against the reference OpenQASM 3 parser. It builds its own virtual environment from the newest wheel in target/wheels, runs each matrix cell in a fresh worker, and samples RSS from a separate coordinator so memory polling does not perturb latency.

OpenQASM parser comparison

  • qdk (This PR)
  • reference openqasm3 1.0.1 (antlr4 4.13.2)
  • Python 3.11.15 on macOS-26.6.1-arm64-arm-64bit

Median latency

Size Statements qdk syntax reference syntax speedup qdk semantic vs reference
10KiB 469 0.49 ms 67.94 ms 140x 0.80 ms 85x
100KiB 4,685 4.65 ms 726 ms 156x 7.35 ms 99x
1MiB 47,984 46.28 ms 8269 ms 179x 77.87 ms 106x
5MiB 239,922 244 ms 46.1 s 189x 399 ms 116x
10MiB 479,843 471 ms 83.2 s 177x 795 ms 105x

Median throughput (MiB/s)

Size qdk syntax reference syntax qdk semantic
10KiB 20.1 0.1 12.2
100KiB 21.0 0.1 13.3
1MiB 21.6 0.1 12.8
5MiB 20.5 0.1 12.5
10MiB 21.2 0.1 12.6

Median throughput (statements/s)

Size Statements qdk syntax reference syntax qdk semantic
10KiB 469 963,864 6,903 583,999
100KiB 4,685 1,008,413 6,454 637,762
1MiB 47,984 1,036,780 5,803 616,181
5MiB 239,922 984,023 5,201 601,238
10MiB 479,843 1,019,029 5,765 603,582

Incremental peak resident set size (RSS)

RSS is the amount of a process's memory currently held in physical RAM, as reported by the OS.

Size qdk syntax reference syntax ratio qdk semantic
10KiB 1.5 MiB 4.6 MiB 3.1x 2.5 MiB
100KiB 6.3 MiB 35.4 MiB 5.6x 13.6 MiB
1MiB 56.6 MiB 362.7 MiB 6.4x 127.3 MiB
5MiB 280.6 MiB 1772.5 MiB 6.3x 632.0 MiB
10MiB 560.8 MiB 3577.5 MiB 6.4x 1263.7 MiB

Peak RSS as a multiple of source size

Size qdk syntax reference syntax qdk semantic
10KiB 154x 470x 261x
100KiB 64x 363x 139x
1MiB 57x 363x 127x
5MiB 56x 355x 126x
10MiB 56x 358x 126x

…of OpenQASM programs, including the AST binding layer, type stubs, tests, and Python API documentation
@idavis
Ian Davis (idavis) force-pushed the iadavis/qdk-openqasm-python branch from 4b137b6 to 5f950b1 Compare August 13, 2026 23:23
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.

1 participant