diff --git a/build.py b/build.py index a721f0ad36..158888c04a 100755 --- a/build.py +++ b/build.py @@ -458,6 +458,14 @@ def run_ci_historic_benchmark(): build_wheel(python_bin, qdk_python_src, env=pip_env, maturin=True) step_end() + if args.check: + step_start("Checking qdk public API surface for private type leakage") + run( + [python_bin, os.path.join(qdk_python_src, "check_api_surface.py")], + cwd=qdk_python_src, + ) + step_end() + if run_tests: step_start("Running tests for the qdk python package") # Install per-package test requirements (pytest, etc.) diff --git a/source/qdk_package/check_api_surface.py b/source/qdk_package/check_api_surface.py new file mode 100644 index 0000000000..cc76006083 --- /dev/null +++ b/source/qdk_package/check_api_surface.py @@ -0,0 +1,434 @@ +#!/usr/bin/env python3 + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Lint: detect private-symbol leakage in the qdk public API surface. + +For every symbol listed in a module's ``__all__``, this script inspects +function / method type annotations. If any annotation references a type +that is **only** reachable through an underscore-prefixed (private) +module path, a violation is reported. + +Base classes are intentionally *not* checked — a private base (mixin, +ABC, protocol) is an implementation detail that users never need to +reference directly, so it does not constitute actionable leakage. + +Types that are defined in a private module but re-exported through a +public module's ``__all__`` are **not** flagged — they are considered +public. + +Exit code 0 - no violations found. +Exit code 1 - one or more violations found (details printed to stderr). + +Usage:: + + python check_api_surface.py # check everything + python check_api_surface.py --json # machine-readable output +""" + +from __future__ import annotations + +import argparse +import importlib +import inspect +import json +import pkgutil +import sys +import types +import typing +from dataclasses import dataclass, field +from typing import get_type_hints + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +# Root package to scan. +ROOT_PACKAGE = "qdk" + +# Modules to skip entirely (they are internal and not expected to have +# a clean public surface). +SKIP_MODULES: set[str] = { + "qdk.telemetry", + "qdk.telemetry_events", +} + +# Only report violations for types whose __module__ starts with one of +# these prefixes. This filters out false positives from standard-library +# and third-party packages whose internal types happen to have private +# __module__ paths (e.g. pathlib._local.Path, concurrent.futures._base.Future, +# qiskit._accelerate.target.QubitProperties). +OWNED_PREFIXES: tuple[str, ...] = ("qdk.",) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _is_private_name(name: str) -> bool: + """Return True if *name* starts with underscore (Python private convention).""" + return name.startswith("_") and not name.startswith("__") + + +def _module_has_private_segment(mod_name: str) -> bool: + """Return True if any segment in a dotted module path is private. + + >>> _module_has_private_segment("qdk._native") + True + >>> _module_has_private_segment("qdk.qsharp") + False + """ + return any(_is_private_name(part) for part in mod_name.split(".")) + + +def _type_fqn(tp: type) -> str: + """Fully-qualified name of a type (best effort).""" + mod = getattr(tp, "__module__", "") or "" + qual = getattr(tp, "__qualname__", "") or getattr(tp, "__name__", str(tp)) + if mod: + return f"{mod}.{qual}" + return qual + + +def _extract_leaf_types(annotation) -> list: + """Recursively unwrap generic aliases and return leaf types.""" + origin = getattr(annotation, "__origin__", None) + args = getattr(annotation, "__args__", None) + + # typing special forms (Union, Optional, etc.) + if origin is typing.Union or origin is types.UnionType: + result = [] + for arg in args or (): + result.extend(_extract_leaf_types(arg)) + return result + + if args: + result = [] + if isinstance(origin, type): + result.append(origin) + for arg in args: + result.extend(_extract_leaf_types(arg)) + return result + + if isinstance(annotation, type): + return [annotation] + + # TypeVar — check bound and constraints + if isinstance(annotation, typing.TypeVar): + result = [] + if annotation.__bound__: + result.extend(_extract_leaf_types(annotation.__bound__)) + for c in annotation.__constraints__: + result.extend(_extract_leaf_types(c)) + return result + + # Forward reference (string annotation) – we can't resolve these + # without the full module context, but check if the string looks private. + if isinstance(annotation, (str, typing.ForwardRef)): + return [annotation] + + return [] + + +@dataclass +class Violation: + """A single API-surface violation.""" + + module: str + public_symbol: str + context: str # e.g. "return type", "parameter 'foo'", "base class" + private_ref: str # the private type or module reference + + def __str__(self) -> str: + return ( + f"{self.module}.{self.public_symbol}: " + f"{self.context} references private {self.private_ref}" + ) + + +def _build_public_types( + modules: list[tuple[str, types.ModuleType]], +) -> tuple[set[int], set[str]]: + """Build the set of types that are publicly re-exported. + + Returns: + A tuple of (public_type_ids, public_type_names) where: + - public_type_ids is a set of ``id()`` values for type objects + found in any public module's ``__all__``. + - public_type_names is a set of unqualified names (e.g. "Config") + for resolving forward-reference strings. + """ + public_type_ids: set[int] = set() + public_type_names: set[str] = set() + + for mod_name, mod in modules: + all_symbols = getattr(mod, "__all__", None) + if all_symbols is None: + continue + for sym_name in all_symbols: + obj = getattr(mod, sym_name, None) + if obj is None: + continue + if isinstance(obj, type): + public_type_ids.add(id(obj)) + public_type_names.add(sym_name) + + return public_type_ids, public_type_names + + +def _check_annotation( + annotation, + module_name: str, + symbol_name: str, + context: str, + violations: list[Violation], + public_type_ids: set[int], + public_type_names: set[str], +) -> None: + """Check a single annotation for private-symbol leakage.""" + for leaf in _extract_leaf_types(annotation): + if isinstance(leaf, (str, typing.ForwardRef)): + ref_str = ( + leaf + if isinstance(leaf, str) + else ( + leaf.__forward_arg__ + if hasattr(leaf, "__forward_arg__") + else str(leaf) + ) + ) + if _is_private_name(ref_str) or any( + _is_private_name(p) for p in ref_str.split(".") + ): + # Check if the bare name matches a publicly-exported type + bare_name = ref_str.rsplit(".", 1)[-1] + if bare_name in public_type_names: + continue + violations.append(Violation(module_name, symbol_name, context, ref_str)) + elif isinstance(leaf, type): + # Skip types that are publicly re-exported + if id(leaf) in public_type_ids: + continue + leaf_mod = getattr(leaf, "__module__", "") or "" + leaf_name = getattr(leaf, "__qualname__", "") or getattr( + leaf, "__name__", "" + ) + # Only flag types owned by this project + if not any(leaf_mod.startswith(p) for p in OWNED_PREFIXES): + continue + if _is_private_name(leaf_name) or _module_has_private_segment(leaf_mod): + ref = _type_fqn(leaf) + violations.append(Violation(module_name, symbol_name, context, ref)) + + +def _check_callable( + obj, + module_name: str, + symbol_name: str, + violations: list[Violation], + public_type_ids: set[int], + public_type_names: set[str], +) -> None: + """Inspect a function or method's annotations for private types.""" + try: + hints = get_type_hints(obj) + except Exception: + # If we can't resolve hints (e.g. forward refs to unavailable types), + # fall back to raw __annotations__. + hints = getattr(obj, "__annotations__", {}) + + for param_name, annotation in hints.items(): + if param_name == "return": + context = "return type" + else: + context = f"parameter '{param_name}'" + _check_annotation( + annotation, + module_name, + symbol_name, + context, + violations, + public_type_ids, + public_type_names, + ) + + +def _check_class( + cls: type, + module_name: str, + symbol_name: str, + violations: list[Violation], + public_type_ids: set[int], + public_type_names: set[str], +) -> None: + """Check a class's public methods' annotations for private types. + + Note: base classes are intentionally *not* checked. A private base + (mixin, ABC, protocol) is an implementation detail that users never + reference directly, so it does not constitute actionable leakage. + """ + # Check public methods — only those defined in qdk-owned modules. + for attr_name in dir(cls): + if attr_name.startswith("_") and not attr_name.startswith("__"): + continue # skip private methods + try: + attr = getattr(cls, attr_name) + except Exception: + continue + if not callable(attr): + continue + if not (inspect.isfunction(attr) or inspect.ismethod(attr)): + continue + # Skip methods inherited from third-party / stdlib classes. + defining_mod = getattr(attr, "__module__", None) or "" + if ( + not any(defining_mod.startswith(p) for p in OWNED_PREFIXES) + and defining_mod != ROOT_PACKAGE + ): + continue + _check_callable( + attr, + module_name, + f"{symbol_name}.{attr_name}", + violations, + public_type_ids, + public_type_names, + ) + + +# --------------------------------------------------------------------------- +# Module discovery and scanning +# --------------------------------------------------------------------------- + + +def _iter_qdk_modules() -> list[tuple[str, types.ModuleType]]: + """Import and yield all public qdk submodules.""" + root = importlib.import_module(ROOT_PACKAGE) + result: list[tuple[str, types.ModuleType]] = [(ROOT_PACKAGE, root)] + + for importer, modname, ispkg in pkgutil.walk_packages( + root.__path__, prefix=ROOT_PACKAGE + "." + ): + # Skip private modules entirely + if any(_is_private_name(part) for part in modname.split(".")): + continue + if modname in SKIP_MODULES: + continue + try: + mod = importlib.import_module(modname) + result.append((modname, mod)) + except Exception as exc: + print(f"WARNING: could not import {modname}: {exc}", file=sys.stderr) + return result + + +def scan() -> list[Violation]: + """Scan the qdk package and return all violations.""" + violations: list[Violation] = [] + + modules = _iter_qdk_modules() + public_type_ids, public_type_names = _build_public_types(modules) + + for mod_name, mod in modules: + all_symbols = getattr(mod, "__all__", None) + if all_symbols is None: + continue # only check modules that declare __all__ + + for sym_name in all_symbols: + obj = getattr(mod, sym_name, None) + if obj is None: + continue + + if isinstance(obj, type): + _check_class( + obj, + mod_name, + sym_name, + violations, + public_type_ids, + public_type_names, + ) + elif callable(obj): + _check_callable( + obj, + mod_name, + sym_name, + violations, + public_type_ids, + public_type_names, + ) + # For non-callable, non-class objects (e.g. TypeVar, constants), + # we skip — they don't have annotations to check. + + return violations + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--json", action="store_true", help="Output violations as JSON") + args = parser.parse_args() + + violations = scan() + + if not violations: + print("No private API leakage detected.", file=sys.stderr) + return 0 + + if args.json: + print( + json.dumps( + [ + { + "module": v.module, + "symbol": v.public_symbol, + "context": v.context, + "private_ref": v.private_ref, + } + for v in violations + ], + indent=2, + ) + ) + else: + # Compute unique private types for the summary line. + unique_types = {v.private_ref for v in violations} + + print( + f"\n{'='*70}\n" + f" Private API leakage detected\n" + f" {len(unique_types)} private type(s), " + f"{len(violations)} reference(s) across public API\n" + f"{'='*70}\n", + file=sys.stderr, + ) + + # Primary grouping: by private type (the actionable unit). + by_type: dict[str, list[Violation]] = {} + for v in violations: + by_type.setdefault(v.private_ref, []).append(v) + + for priv_type, vs in sorted(by_type.items()): + print( + f"\n {priv_type} ({len(vs)} reference(s))", + file=sys.stderr, + ) + for v in vs: + print( + f" - {v.module}.{v.public_symbol}: {v.context}", + file=sys.stderr, + ) + + print(f"\n{'='*70}\n", file=sys.stderr) + + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/source/qdk_package/qdk/_context.py b/source/qdk_package/qdk/_context.py index a0508de548..07d6757ec9 100644 --- a/source/qdk_package/qdk/_context.py +++ b/source/qdk_package/qdk/_context.py @@ -558,7 +558,9 @@ def run( :param *args: The arguments to pass to the callable, if one is provided. :param on_result: A callback function that will be called with each result. :param save_events: If true, the output of each shot will be saved. If false, they will be printed. - :param noise: The noise to use in simulation. + :param noise: The noise to use in simulation. Can be a tuple of ``(x, y, z)`` + Pauli error probabilities, a :class:`~qdk.qsharp.PauliNoise` subclass, or a + :class:`~qdk.simulation.NoiseConfig` for per-gate noise configuration. :param qubit_loss: The probability of qubit loss in simulation. :param seed: The seed to use for the random number generator in simulation, if any. :param type: The type of simulator to use. If not specified, the default sparse state vector simulation will be used. diff --git a/source/qdk_package/qdk/_interpreter.py b/source/qdk_package/qdk/_interpreter.py index 01561f5065..debdd1dc75 100644 --- a/source/qdk_package/qdk/_interpreter.py +++ b/source/qdk_package/qdk/_interpreter.py @@ -235,7 +235,9 @@ def run( :param *args: The arguments to pass to the callable, if one is provided. :param on_result: A callback function that will be called with each result. :param save_events: If true, the output of each shot will be saved. If false, they will be printed. - :param noise: The noise to use in simulation. + :param noise: The noise to use in simulation. Can be a tuple of ``(x, y, z)`` + Pauli error probabilities, a :class:`~qdk.qsharp.PauliNoise` subclass, or a + :class:`~qdk.simulation.NoiseConfig` for per-gate noise configuration. :param qubit_loss: The probability of qubit loss in simulation. :param seed: The seed to use for the random number generator in simulation, if any. :param type: The type of simulator to use. If not specified, the default sparse state vector simulation will be used. diff --git a/source/qdk_package/qdk/internal.py b/source/qdk_package/qdk/internal.py new file mode 100644 index 0000000000..3a2f94acbc --- /dev/null +++ b/source/qdk_package/qdk/internal.py @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Internal types that appear in the public API surface. + +.. warning:: + The types exposed here are **not** part of the supported public API + and may change in any release without notice. They are made reachable + from this module solely so that: + + 1. Documentation generators (py2docfx, Sphinx) can emit working + cross-reference links for return types and parameter types. + 2. Type checkers (pyright, mypy) do not flag references as + private-module accesses when users annotate variables that hold + values returned by public functions. + 3. Users who follow a type annotation can land on a clearly-labeled + page rather than a ``ModuleNotFoundError``. + + Do **not** depend on the presence or shape of any symbol in this + module. If you need to construct or configure one of these types + directly, use the corresponding public API instead. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from enum import Enum + from typing import Any, Dict, Optional, Protocol, Union + + class StateDumpData(Protocol): + """A state dump returned from the Q# interpreter.""" + + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def _repr_markdown_(self) -> str: ... + def _repr_latex_(self) -> Optional[str]: ... + + class Circuit(Protocol): + """A quantum circuit diagram generated from a Q# or OpenQASM program.""" + + def json(self) -> str: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + + class Closure(Protocol): + """An opaque closure reference that can be passed back into Q#.""" + + ... + + class GlobalCallable(Protocol): + """An opaque callable reference that can be passed back into Q#.""" + + ... + + class Output(Protocol): + """An output returned from the Q# interpreter. + + Outputs can be state dumps, matrices, or messages. + """ + + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def _repr_markdown_(self) -> Optional[str]: ... + + class Config(Protocol): + """Configuration hints for the language service.""" + + def __repr__(self) -> str: ... + def _repr_mimebundle_( + self, + include: Union[Any, None] = None, + exclude: Union[Any, None] = None, + ) -> Dict[str, Dict[str, Any]]: ... + + class QirInputData(Protocol): + """Wraps a compiled QIR program for submission to a quantum target. + + Implements the ``QirRepresentable`` protocol expected by the + ``azure-quantum`` package. + """ + + _name: str + + def _repr_qir_(self, **kwargs) -> bytes: ... + def __str__(self) -> str: ... + + class ZoneType(Enum): + """Type of zone in a neutral-atom device layout.""" + + REG = "register" + INTER = "interaction" + MEAS = "measurement" + + class Zone(Protocol): + """A zone in a neutral-atom device layout.""" + + name: str + row_count: int + type: ZoneType + offset: int + + def set_offset(self, offset: int) -> None: ... + +else: + from ._native import ( # type: ignore + Circuit, + Closure, + GlobalCallable, + Output, + StateDumpData, + ) + from ._types import ( + Config, + QirInputData, + ) + from ._device._device import Zone, ZoneType + +__all__ = [ + "Circuit", + "Closure", + "Config", + "GlobalCallable", + "Output", + "QirInputData", + "StateDumpData", + "Zone", + "ZoneType", +] diff --git a/source/qdk_package/qdk/qre/__init__.py b/source/qdk_package/qdk/qre/__init__.py index 0dbe8d4a9d..90ce7e28e2 100644 --- a/source/qdk_package/qdk/qre/__init__.py +++ b/source/qdk_package/qdk/qre/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from ._application import Application +from ._application import Application, TraceParameters from ._architecture import Architecture, ISAContext from ._estimation import estimate from ._instruction import ( @@ -79,6 +79,7 @@ "property_name", "property_name_to_key", "Trace", + "TraceParameters", "TraceQuery", "TraceTransform", "LOGICAL", diff --git a/source/qdk_package/qdk/qre/_application.py b/source/qdk_package/qdk/qre/_application.py index 6c20621b2b..05a5357977 100644 --- a/source/qdk_package/qdk/qre/_application.py +++ b/source/qdk_package/qdk/qre/_application.py @@ -70,9 +70,9 @@ def q(**kwargs) -> TraceQuery: """ return TraceQuery(NoneType, **kwargs) - def context(self) -> _Context: + def context(self) -> ApplicationContext: """Create a new enumeration context for this application.""" - return _Context(self) + return ApplicationContext(self) def post_process( self, parameters: TraceParameters, estimation: EstimationResult @@ -157,7 +157,7 @@ def disable_parallel_traces(self): self._parallel_traces = False -class _Context: +class ApplicationContext: """Enumeration context wrapping an application instance.""" application: Application diff --git a/source/qdk_package/qdk/qre/_instruction.py b/source/qdk_package/qdk/qre/_instruction.py index 4669a86d4c..a665bf1bc3 100644 --- a/source/qdk_package/qdk/qre/_instruction.py +++ b/source/qdk_package/qdk/qre/_instruction.py @@ -171,7 +171,7 @@ def q(cls, *, source: ISAQuery | None = None, **kwargs) -> ISAQuery: ) @classmethod - def bind(cls, name: str, node: ISAQuery) -> _BindingNode: + def bind(cls, name: str, node: ISAQuery) -> ISAQuery: """ Create a BindingNode for this transform. @@ -182,7 +182,7 @@ def bind(cls, name: str, node: ISAQuery) -> _BindingNode: node (Node): The child node that can reference this binding. Returns: - BindingNode: A binding node enclosing this transform. + ISAQuery: A binding node enclosing this transform. """ return cls.q().bind(name, node) diff --git a/source/qdk_package/qdk/qre/_isa_enumeration.py b/source/qdk_package/qdk/qre/_isa_enumeration.py index 7543c071ed..9c93f2fff2 100644 --- a/source/qdk_package/qdk/qre/_isa_enumeration.py +++ b/source/qdk_package/qdk/qre/_isa_enumeration.py @@ -61,7 +61,7 @@ def populate(self, ctx: ISAContext) -> int: pass return start - def __add__(self, other: ISAQuery) -> _SumNode: + def __add__(self, other: ISAQuery) -> ISAQuery: """ Perform a union of two enumeration nodes. @@ -73,7 +73,7 @@ def __add__(self, other: ISAQuery) -> _SumNode: other (Node): The other enumeration node. Returns: - SumNode: A node representing the union of both enumerations. + ISAQuery: A node representing the union of both enumerations. Example: @@ -95,7 +95,7 @@ def __add__(self, other: ISAQuery) -> _SumNode: else: return _SumNode([self, other]) - def __mul__(self, other: ISAQuery) -> _ProductNode: + def __mul__(self, other: ISAQuery) -> ISAQuery: """ Perform the cross product of two enumeration nodes. @@ -109,7 +109,7 @@ def __mul__(self, other: ISAQuery) -> _ProductNode: other (Node): The other enumeration node. Returns: - ProductNode: A node representing the product of both enumerations. + ISAQuery: A node representing the product of both enumerations. Example: @@ -133,7 +133,7 @@ def __mul__(self, other: ISAQuery) -> _ProductNode: else: return _ProductNode([self, other]) - def bind(self, name: str, node: ISAQuery) -> "_BindingNode": + def bind(self, name: str, node: ISAQuery) -> ISAQuery: """Create a BindingNode with this node as the component. Args: @@ -141,7 +141,7 @@ def bind(self, name: str, node: ISAQuery) -> "_BindingNode": node: The child enumeration node that may contain ISARefNodes. Returns: - A BindingNode with self as the component. + ISAQuery: A binding node with self as the component. Example: diff --git a/source/qdk_package/qdk/qre/_trace.py b/source/qdk_package/qdk/qre/_trace.py index 49974e80d9..3ecd52cc95 100644 --- a/source/qdk_package/qdk/qre/_trace.py +++ b/source/qdk_package/qdk/qre/_trace.py @@ -9,7 +9,7 @@ from typing import Any, Optional, Generator, Type, TYPE_CHECKING if TYPE_CHECKING: - from ._application import _Context + from ._application import ApplicationContext from ._enumeration import _enumerate_instances from ._qre import PSSPC as _PSSPC, LatticeSurgery as _LatticeSurgery, Trace @@ -113,7 +113,7 @@ class _Node(ABC): """Abstract base class for trace enumeration nodes.""" @abstractmethod - def enumerate(self, ctx: _Context) -> Generator[Trace, None, None]: ... + def enumerate(self, ctx: ApplicationContext) -> Generator[Trace, None, None]: ... class TraceQuery(_Node): @@ -131,12 +131,12 @@ def __init__(self, t: Type, **kwargs): self.sequence = [(t, kwargs)] def enumerate( - self, ctx: _Context, track_parameters: bool = False + self, ctx: ApplicationContext, track_parameters: bool = False ) -> Generator[Trace | tuple[Any, Trace], None, None]: """Enumerate transformed traces from the application context. Args: - ctx (_Context): The application enumeration context. + ctx (ApplicationContext): The application enumeration context. track_parameters (bool): If True, yield ``(parameters, trace)`` tuples instead of plain traces. Default is False. diff --git a/source/qdk_package/qdk/qre/internal.py b/source/qdk_package/qdk/qre/internal.py new file mode 100644 index 0000000000..c1b2615d7e --- /dev/null +++ b/source/qdk_package/qdk/qre/internal.py @@ -0,0 +1,102 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Internal types that appear in the ``qdk.qre`` public API surface. + +.. warning:: + The types exposed here are **not** part of the supported public API + and may change in any release without notice. They are made reachable + from this module solely so that: + + 1. Documentation generators (py2docfx, Sphinx) can emit working + cross-reference links for return types and parameter types. + 2. Type checkers (pyright, mypy) do not flag references as + private-module accesses when users annotate variables that hold + values returned by public functions. + 3. Users who follow a type annotation can land on a clearly-labeled + page rather than a ``ModuleNotFoundError``. + + Do **not** depend on the presence or shape of any symbol in this + module. If you need to construct or configure one of these types + directly, use the corresponding public API instead. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import ClassVar, Optional, Protocol, Union + + from ._architecture import Architecture + from ._instruction import ISATransform + + # ------------------------------------------------------------------ + # ApplicationContext + # (runtime: _application.ApplicationContext) + # ------------------------------------------------------------------ + class ApplicationContext(Protocol): + """Enumeration context wrapping an application instance. + + Obtained via :meth:`Application.context` and passed to + :meth:`TraceQuery.enumerate`. + """ + + @property + def application(self) -> "Application": ... + + # ------------------------------------------------------------------ + # DataclassProtocol + # (runtime: _application.DataclassProtocol) + # ------------------------------------------------------------------ + class DataclassProtocol(Protocol): + """Structural type satisfied by any ``@dataclass`` class. + + Used as a constraint on :data:`TraceParameters`. + """ + + __dataclass_fields__: ClassVar[dict] + + # ------------------------------------------------------------------ + # InstructionSourceNodeReference + # (runtime: _instruction._InstructionSourceNodeReference) + # ------------------------------------------------------------------ + class InstructionSourceNodeReference(Protocol): + """Reference to a node in an ``InstructionSource`` graph.""" + + @property + def instruction(self) -> Instruction: ... + @property + def transform(self) -> Optional[Union[ISATransform, Architecture]]: ... + + # ------------------------------------------------------------------ + # Instruction (runtime: _qre.Instruction — Rust native) + # ------------------------------------------------------------------ + class Instruction(Protocol): + """A quantum instruction with resource properties.""" + + @property + def id(self) -> int: ... + @property + def encoding(self) -> int: ... + @property + def arity(self) -> Optional[int]: ... + def space(self, arity: Optional[int] = None) -> Optional[int]: ... + def time(self, arity: Optional[int] = None) -> Optional[int]: ... + def error_rate(self, arity: Optional[int] = None) -> Optional[float]: ... + def expect_time(self, arity: Optional[int] = None) -> int: ... + def expect_error_rate(self, arity: Optional[int] = None) -> float: ... + +else: + from ._application import ApplicationContext, DataclassProtocol + from ._instruction import ( + _InstructionSourceNodeReference as InstructionSourceNodeReference, + ) + from ._qre import Instruction + +__all__ = [ + "ApplicationContext", + "DataclassProtocol", + "Instruction", + "InstructionSourceNodeReference", +]