From 057507dab6729b1720d4c588b81901b1793c1e69 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Mon, 23 Feb 2026 13:59:58 +0000 Subject: [PATCH 01/17] Add draft metadata specification --- .../src/guppylang_internals/debug_info.py | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 guppylang-internals/src/guppylang_internals/debug_info.py diff --git a/guppylang-internals/src/guppylang_internals/debug_info.py b/guppylang-internals/src/guppylang_internals/debug_info.py new file mode 100644 index 000000000..dbfb3c90c --- /dev/null +++ b/guppylang-internals/src/guppylang_internals/debug_info.py @@ -0,0 +1,144 @@ +from abc import ABC, abstractmethod +from collections.abc import Mapping +from dataclasses import dataclass + +from hugr.metadata import Metadata + +# Type alias for json values. +# See +JsonType = str | int | float | bool | None | Mapping[str, "JsonType"] | list["JsonType"] + + +@dataclass +class DebugRecord(ABC): + """Abstract base class for debug records.""" + + @abstractmethod + def to_json(self) -> JsonType: + """Encodes the record as a dictionary of native types that can be serialized by + `json.dump`. + """ + + @classmethod + @abstractmethod + def from_json(cls, value: JsonType) -> "DebugRecord": + """Decodes the extension from a native types obtained from `json.load`.""" + + +class HugrDebugInfo(Metadata[DebugRecord]): + """Metadata storing debug information for a node.""" + + KEY = "core.debug_info" + + @classmethod + def to_json(cls, value: DebugRecord) -> JsonType: + return value.to_json() + + @classmethod + def from_json(cls, value: JsonType) -> DebugRecord: + return DebugRecord.from_json(value) + + +@dataclass +class DICompileUnit(DebugRecord): + """Debug information for a compilation unit, corresponds to a module node.""" + + directory: str + filename: int # File that contains Hugr entrypoint. + file_table: list[str] # Global table of all files referenced in the module. + + def to_json(self) -> dict[str, JsonType]: + return { + "directory": self.directory, + "filename": self.filename, + "file_table": self.file_table, + } + + @classmethod + def from_json(cls, value: JsonType) -> "DICompileUnit": + if not isinstance(value, dict): + msg = f"Expected a dictionary for DICompileUnit, but got {type(value)}" + raise TypeError(msg) + for key in ("directory", "filename", "file_table"): + if key not in value: + msg = f"Expected DICompileUnit to have a '{key}' key but got {value}" + raise TypeError(msg) + files = value["file_table"] + if not isinstance(files, list): + msg = f"Expected 'file_table' to be a list but got {type(files)}" + raise TypeError(msg) + return DICompileUnit( + directory=str(value["directory"]), + filename=int(value["filename"]), + file_table=list[str](value["file_table"]), + ) + + def get_file_index(self, filename: str) -> int: + for idx, name in enumerate(self.file_table): + if name == filename: + return idx + else: + idx = len(self.file_table) + self.file_table.append(filename) + return idx + + def get_filename(self, idx: int) -> str: + return self.file_table[idx] + + +@dataclass +class DISubprogram(DebugRecord): + """Debug information for a subprogram, corresponds to a function definition or + declaration node.""" + + file: int # Index into the string table for filenames. + line_no: int # First line of the function definition. + scope_line: int # First line of the function body. + + def to_json(self) -> dict[str, str]: + return { + "file": str(self.file), + "line_no": str(self.line_no), + "scope_line": str(self.scope_line), + } + + @classmethod + def from_json(cls, value: JsonType) -> "DISubprogram": + if not isinstance(value, dict): + msg = f"Expected a dictionary for DISubprogram, but got {type(value)}" + raise TypeError(msg) + for key in ("file", "line_no", "scope_line"): + if key not in value: + msg = f"Expected DISubprogram to have a '{key}' key but got {value}" + raise TypeError(msg) + return DISubprogram( + file=int(value["file"]), + line_no=int(value["line_no"]), + scope_line=int(value["scope_line"]), + ) + + +@dataclass +class DILocation(DebugRecord): + """Debug information for a location, corresponds to call or extension operation + node.""" + + column: int + line_no: int + + def to_json(self) -> dict[str, str]: + return { + "column": str(self.column), + "lline_noe": str(self.line_no), + } + + @classmethod + def from_json(cls, value: JsonType) -> "DILocation": + if not isinstance(value, dict): + msg = f"Expected a dictionary for DILocation, but got {type(value)}" + raise TypeError(msg) + for key in ("column", "line_no"): + if key not in value: + msg = f"Expected DILocation to have a '{key}' key but got {value}" + raise TypeError(msg) + return DILocation(column=int(value["column"]), line_no=int(value["line"])) From a1a514513d4e2fd25e7c167dae134802009ae2d1 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Tue, 24 Feb 2026 10:21:08 +0000 Subject: [PATCH 02/17] Add module info --- .../src/guppylang_internals/debug_info.py | 8 ++------ .../src/guppylang_internals/engine.py | 15 +++++++++++++++ tests/test_debug_info.py | 13 +++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 tests/test_debug_info.py diff --git a/guppylang-internals/src/guppylang_internals/debug_info.py b/guppylang-internals/src/guppylang_internals/debug_info.py index dbfb3c90c..27114a83b 100644 --- a/guppylang-internals/src/guppylang_internals/debug_info.py +++ b/guppylang-internals/src/guppylang_internals/debug_info.py @@ -1,12 +1,7 @@ from abc import ABC, abstractmethod -from collections.abc import Mapping from dataclasses import dataclass -from hugr.metadata import Metadata - -# Type alias for json values. -# See -JsonType = str | int | float | bool | None | Mapping[str, "JsonType"] | list["JsonType"] +from hugr.metadata import JsonType, Metadata @dataclass @@ -51,6 +46,7 @@ def to_json(self) -> dict[str, JsonType]: return { "directory": self.directory, "filename": self.filename, + # TODO: Fix table conversion / typing. "file_table": self.file_table, } diff --git a/guppylang-internals/src/guppylang_internals/engine.py b/guppylang-internals/src/guppylang_internals/engine.py index 04b00fb14..1cb215e72 100644 --- a/guppylang-internals/src/guppylang_internals/engine.py +++ b/guppylang-internals/src/guppylang_internals/engine.py @@ -1,4 +1,5 @@ from collections import defaultdict +from pathlib import Path from types import FrameType from typing import TYPE_CHECKING @@ -12,6 +13,7 @@ from semver import Version import guppylang_internals +from guppylang_internals.debug_info import DICompileUnit, HugrDebugInfo from guppylang_internals.definition.common import ( CheckableDef, CheckedDef, @@ -28,6 +30,7 @@ ) from guppylang_internals.error import pretty_errors from guppylang_internals.span import SourceMap +from guppylang_internals.tracing.util import get_calling_frame from guppylang_internals.tys.builtin import ( array_type_def, bool_type_def, @@ -280,6 +283,18 @@ def compile(self, id: DefId) -> ModulePointer: graph = hf.Module() graph.metadata["name"] = "__main__" # entrypoint metadata + # Add debug info about the module to the root node + frame = get_calling_frame() + assert frame is not None + filename = frame.f_code.co_filename + module_info = DICompileUnit( + directory=Path.cwd().as_uri(), + # We know this file is always the first entry in the file table. + filename=0, + file_table=[filename], + ) + graph.hugr.module_root.metadata[HugrDebugInfo] = module_info + # Lower definitions to Hugr from guppylang_internals.compiler.core import CompilerContext diff --git a/tests/test_debug_info.py b/tests/test_debug_info.py new file mode 100644 index 000000000..92cb054de --- /dev/null +++ b/tests/test_debug_info.py @@ -0,0 +1,13 @@ +from guppylang_internals.debug_info import HugrDebugInfo + +from guppylang import guppy + + +def test_compile_unit(): + @guppy + def foo() -> None: + pass + + hugr = foo.compile().modules[0] + meta = hugr.module_root.metadata + assert HugrDebugInfo.KEY in meta From 83bd3d002ea7ae5186db4b0fc7b0d77e9a073799 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Mon, 2 Mar 2026 16:11:34 +0000 Subject: [PATCH 03/17] Refactor existing metadata code --- .../compiler/modifier_compiler.py | 2 +- .../definition/function.py | 2 +- .../definition/metadata.py | 87 ------------------- .../src/guppylang_internals/engine.py | 2 +- .../guppylang_internals/metadata/__init__.py | 0 .../guppylang_internals/metadata/common.py | 84 ++++++++++++++++++ .../{ => metadata}/debug_info.py | 0 .../metadata/max_qubits.py | 19 ++++ guppylang/src/guppylang/decorator.py | 4 +- guppylang/src/guppylang/defs.py | 2 +- tests/definition/__init__.py | 1 - tests/metadata/__init__.py | 0 tests/{ => metadata}/test_debug_info.py | 2 +- .../{definition => metadata}/test_metadata.py | 30 ++++--- tests/{ => metadata}/test_version_metadata.py | 0 15 files changed, 126 insertions(+), 109 deletions(-) delete mode 100644 guppylang-internals/src/guppylang_internals/definition/metadata.py create mode 100644 guppylang-internals/src/guppylang_internals/metadata/__init__.py create mode 100644 guppylang-internals/src/guppylang_internals/metadata/common.py rename guppylang-internals/src/guppylang_internals/{ => metadata}/debug_info.py (100%) create mode 100644 guppylang-internals/src/guppylang_internals/metadata/max_qubits.py delete mode 100644 tests/definition/__init__.py create mode 100644 tests/metadata/__init__.py rename tests/{ => metadata}/test_debug_info.py (76%) rename tests/{definition => metadata}/test_metadata.py (74%) rename tests/{ => metadata}/test_version_metadata.py (100%) diff --git a/guppylang-internals/src/guppylang_internals/compiler/modifier_compiler.py b/guppylang-internals/src/guppylang_internals/compiler/modifier_compiler.py index b64578267..a0413464e 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/modifier_compiler.py +++ b/guppylang-internals/src/guppylang_internals/compiler/modifier_compiler.py @@ -8,7 +8,7 @@ from guppylang_internals.compiler.cfg_compiler import compile_cfg from guppylang_internals.compiler.core import CompilerContext, DFContainer from guppylang_internals.compiler.expr_compiler import ExprCompiler -from guppylang_internals.definition.metadata import add_metadata +from guppylang_internals.metadata.common import add_metadata from guppylang_internals.nodes import CheckedModifiedBlock, PlaceNode from guppylang_internals.std._internal.compiler.array import ( array_new, diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index b7abcc12a..d16c4d3d0 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -34,7 +34,6 @@ RawDef, UnknownSourceError, ) -from guppylang_internals.definition.metadata import GuppyMetadata, add_metadata from guppylang_internals.definition.value import ( CallableDef, CallReturnWires, @@ -42,6 +41,7 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import GuppyError +from guppylang_internals.metadata.common import GuppyMetadata, add_metadata from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.tys.subst import Inst, Subst diff --git a/guppylang-internals/src/guppylang_internals/definition/metadata.py b/guppylang-internals/src/guppylang_internals/definition/metadata.py deleted file mode 100644 index 4d0280575..000000000 --- a/guppylang-internals/src/guppylang_internals/definition/metadata.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Metadata attached to objects within the Guppy compiler, both for internal use and to -attach to HUGR nodes for lower-level processing.""" - -from abc import ABC -from dataclasses import dataclass, field, fields -from typing import Any, ClassVar, Generic, TypeVar - -from hugr.hugr.node_port import ToNode - -from guppylang_internals.diagnostic import Fatal -from guppylang_internals.error import GuppyError - -T = TypeVar("T") - - -@dataclass(init=True, kw_only=True) -class GuppyMetadataValue(ABC, Generic[T]): - """A template class for a metadata value within the scope of the Guppy compiler. - Implementations should provide the `key` in reverse-URL format.""" - - key: ClassVar[str] - value: T | None = None - - -class MetadataMaxQubits(GuppyMetadataValue[int]): - key = "tket.hint.max_qubits" - - -@dataclass(frozen=True, init=True, kw_only=True) -class GuppyMetadata: - """DTO for metadata within the scope of the guppy compiler for attachment to HUGR - nodes. See `add_metadata`.""" - - max_qubits: MetadataMaxQubits = field(default_factory=MetadataMaxQubits, init=False) - - @classmethod - def reserved_keys(cls) -> set[str]: - return {f.type.key for f in fields(GuppyMetadata)} # type: ignore[union-attr] - - -@dataclass(frozen=True) -class MetadataAlreadySetError(Fatal): - title: ClassVar[str] = "Metadata key already set" - message: ClassVar[str] = "Received two values for the metadata key `{key}`" - key: str - - -@dataclass(frozen=True) -class ReservedMetadataKeysError(Fatal): - title: ClassVar[str] = "Metadata key is reserved" - message: ClassVar[str] = ( - "The following metadata keys are reserved by Guppy but also provided in " - "additional metadata: `{keys}`" - ) - keys: set[str] - - -def add_metadata( - node: ToNode, - metadata: GuppyMetadata | None = None, - *, - additional_metadata: dict[str, Any] | None = None, -) -> None: - """Adds metadata to the given node using the keys defined through inheritors of - `GuppyMetadataValue` defined in the `GuppyMetadata` class. - - Additional metadata is forwarded as is, although the given dictionary may not - contain any keys already reserved by fields in `GuppyMetadata`. - """ - if metadata is not None: - for f in fields(GuppyMetadata): - data: GuppyMetadataValue[Any] = getattr(metadata, f.name) - if data.key in node.metadata: - raise GuppyError(MetadataAlreadySetError(None, data.key)) - if data.value is not None: - node.metadata[data.key] = data.value - - if additional_metadata is not None: - reserved_keys = GuppyMetadata.reserved_keys() - used_reserved_keys = reserved_keys.intersection(additional_metadata.keys()) - if len(used_reserved_keys) > 0: - raise GuppyError(ReservedMetadataKeysError(None, keys=used_reserved_keys)) - - for key, value in additional_metadata.items(): - if key in node.metadata: - raise GuppyError(MetadataAlreadySetError(None, key)) - node.metadata[key] = value diff --git a/guppylang-internals/src/guppylang_internals/engine.py b/guppylang-internals/src/guppylang_internals/engine.py index 1cb215e72..e980dc45c 100644 --- a/guppylang-internals/src/guppylang_internals/engine.py +++ b/guppylang-internals/src/guppylang_internals/engine.py @@ -13,7 +13,6 @@ from semver import Version import guppylang_internals -from guppylang_internals.debug_info import DICompileUnit, HugrDebugInfo from guppylang_internals.definition.common import ( CheckableDef, CheckedDef, @@ -29,6 +28,7 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import pretty_errors +from guppylang_internals.metadata.debug_info import DICompileUnit, HugrDebugInfo from guppylang_internals.span import SourceMap from guppylang_internals.tracing.util import get_calling_frame from guppylang_internals.tys.builtin import ( diff --git a/guppylang-internals/src/guppylang_internals/metadata/__init__.py b/guppylang-internals/src/guppylang_internals/metadata/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/guppylang-internals/src/guppylang_internals/metadata/common.py b/guppylang-internals/src/guppylang_internals/metadata/common.py new file mode 100644 index 000000000..431a90067 --- /dev/null +++ b/guppylang-internals/src/guppylang_internals/metadata/common.py @@ -0,0 +1,84 @@ +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from hugr.hugr.node_port import ToNode +from hugr.metadata import JsonType, Metadata, NodeMetadata + +from guppylang_internals.diagnostic import Fatal +from guppylang_internals.error import GuppyError +from guppylang_internals.metadata.debug_info import DebugRecord, HugrDebugInfo +from guppylang_internals.metadata.max_qubits import MetadataMaxQubits + + +@dataclass(frozen=True) +class MetadataAlreadySetError(Fatal): + title: ClassVar[str] = "Metadata key already set" + message: ClassVar[str] = "Received two values for the metadata key `{key}`" + key: str + + +@dataclass(frozen=True) +class ReservedMetadataKeysError(Fatal): + title: ClassVar[str] = "Metadata key is reserved" + message: ClassVar[str] = ( + "The following metadata keys are reserved by Guppy but also provided in " + "additional metadata: `{keys}`" + ) + keys: set[str] + + +@dataclass +class GuppyMetadata: + """Class for storing metadata to be attached to Hugr nodes during compilation.""" + + _node_metadata: dict[str, JsonType] = field(default_factory=dict) + _RESERVED_KEYS: ClassVar[frozenset[type[Metadata[Any]]]] = { + HugrDebugInfo, + MetadataMaxQubits, + } + + def as_dict(self) -> dict[str, JsonType]: + return self._node_metadata + + def set_debug_info(self, debug_info: DebugRecord) -> None: + self._node_metadata[HugrDebugInfo.KEY] = debug_info + + def set_max_qubits(self, max_qubits: int) -> None: + self._node_metadata[MetadataMaxQubits.KEY] = max_qubits + + def get_debug_info(self) -> DebugRecord | None: + return self._node_metadata.get(HugrDebugInfo.KEY) + + def get_max_qubits(self) -> int | None: + return self._node_metadata.get(MetadataMaxQubits.KEY) + + @classmethod + def reserved_keys(cls) -> set[str]: + return {t.KEY for t in cls._RESERVED_KEYS} + + +def add_metadata( + node: ToNode, + metadata: GuppyMetadata | None = None, + *, + additional_metadata: dict[str, Any] | None = None, +) -> None: + """Extends metadata of a node, ensuring reserved keys aren't overwritten.""" + if metadata is not None: + metadata_dict = metadata.as_dict() + for key in metadata_dict: + if key in node.metadata: + raise GuppyError(MetadataAlreadySetError(None, key)) + if metadata_dict[key] is not None: + node.metadata[key] = metadata_dict[key] + + if additional_metadata is not None: + reserved_keys = GuppyMetadata.reserved_keys() + used_reserved_keys = reserved_keys.intersection(additional_metadata.keys()) + if len(used_reserved_keys) > 0: + raise GuppyError(ReservedMetadataKeysError(None, keys=used_reserved_keys)) + + for key, value in additional_metadata.items(): + if key in node.metadata: + raise GuppyError(MetadataAlreadySetError(None, key)) + node.metadata[key] = value diff --git a/guppylang-internals/src/guppylang_internals/debug_info.py b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py similarity index 100% rename from guppylang-internals/src/guppylang_internals/debug_info.py rename to guppylang-internals/src/guppylang_internals/metadata/debug_info.py diff --git a/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py b/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py new file mode 100644 index 000000000..f2243a66a --- /dev/null +++ b/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +from hugr.metadata import JsonType, Metadata + + +@dataclass(frozen=True) +class MetadataMaxQubits(Metadata[int]): + KEY = "tket.hint.max_qubits" + + @classmethod + def to_json(cls, value: int) -> JsonType: + return value + + @classmethod + def from_json(cls, value: JsonType) -> int: + if not isinstance(value, int): + msg = f"Expected an integer for MetadataMaxQubits, but got {type(value)}" + raise TypeError(msg) + return value diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 8f90e6374..5140fae94 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -26,7 +26,7 @@ from guppylang_internals.definition.function import ( RawFunctionDef, ) -from guppylang_internals.definition.metadata import GuppyMetadata +from guppylang_internals.metadata.common import GuppyMetadata from guppylang_internals.definition.overloaded import OverloadedFunctionDef from guppylang_internals.definition.parameter import ( ConstVarDef, @@ -661,7 +661,7 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> tuple[UnitaryFlags, GuppyMetadata]: flags |= UnitaryFlags.Power metadata = GuppyMetadata() - metadata.max_qubits.value = kwargs.pop("max_qubits", None) + metadata.set_max_qubits(kwargs.pop("max_qubits", None)) if remaining := next(iter(kwargs), None): err = f"Unknown keyword argument: `{remaining}`" diff --git a/guppylang/src/guppylang/defs.py b/guppylang/src/guppylang/defs.py index 4dc10ccc8..e0368d9de 100644 --- a/guppylang/src/guppylang/defs.py +++ b/guppylang/src/guppylang/defs.py @@ -121,7 +121,7 @@ def emulator( isinstance(self.wrapped, RawFunctionDef) and self.wrapped.metadata is not None ): - hinted_qubits = self.wrapped.metadata.max_qubits.value + hinted_qubits = self.wrapped.metadata.get_max_qubits() if qubits is None: qubits = hinted_qubits elif hinted_qubits is not None and qubits < hinted_qubits: diff --git a/tests/definition/__init__.py b/tests/definition/__init__.py deleted file mode 100644 index c922290ad..000000000 --- a/tests/definition/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Unit tests for guppylang_internals.definition diff --git a/tests/metadata/__init__.py b/tests/metadata/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_debug_info.py b/tests/metadata/test_debug_info.py similarity index 76% rename from tests/test_debug_info.py rename to tests/metadata/test_debug_info.py index 92cb054de..74c2224ca 100644 --- a/tests/test_debug_info.py +++ b/tests/metadata/test_debug_info.py @@ -1,4 +1,4 @@ -from guppylang_internals.debug_info import HugrDebugInfo +from guppylang_internals.metadata.debug_info import HugrDebugInfo from guppylang import guppy diff --git a/tests/definition/test_metadata.py b/tests/metadata/test_metadata.py similarity index 74% rename from tests/definition/test_metadata.py rename to tests/metadata/test_metadata.py index a3fc5e479..ee734e6f0 100644 --- a/tests/definition/test_metadata.py +++ b/tests/metadata/test_metadata.py @@ -3,24 +3,25 @@ from unittest.mock import Mock import pytest -from guppylang_internals.definition.metadata import ( +from guppylang_internals.error import GuppyError +from guppylang_internals.metadata.common import ( GuppyMetadata, MetadataAlreadySetError, ReservedMetadataKeysError, add_metadata, ) -from guppylang_internals.error import GuppyError +from hugr.metadata import NodeMetadata def test_add_metadata(): mock_hugr_node = Mock() - mock_hugr_node.metadata = {"some-key": "some-value"} + mock_hugr_node.metadata = NodeMetadata({"some-key": "some-value"}) guppy_metadata = GuppyMetadata() - guppy_metadata.max_qubits.value = 5 + guppy_metadata.set_max_qubits(5) add_metadata(mock_hugr_node, guppy_metadata) - assert mock_hugr_node.metadata == { + assert mock_hugr_node.metadata.as_dict() == { "some-key": "some-value", "tket.hint.max_qubits": 5, } @@ -28,11 +29,11 @@ def test_add_metadata(): def test_add_additional_metadata(): mock_hugr_node = Mock() - mock_hugr_node.metadata = {"some-key": "some-value"} + mock_hugr_node.metadata = NodeMetadata({"some-key": "some-value"}) add_metadata(mock_hugr_node, additional_metadata={"more-key": "more-value"}) - assert mock_hugr_node.metadata == { + assert mock_hugr_node.metadata.as_dict() == { "some-key": "some-value", "more-key": "more-value", } @@ -40,7 +41,7 @@ def test_add_additional_metadata(): def test_add_metadata_no_reserved_metadata(): mock_hugr_node = Mock() - mock_hugr_node.metadata = {} + mock_hugr_node.metadata = NodeMetadata({}) with pytest.raises( GuppyError, @@ -54,13 +55,14 @@ def test_add_metadata_no_reserved_metadata(): def test_add_metadata_metadata_already_set(): mock_hugr_node = Mock() - mock_hugr_node.metadata = { + mock_hugr_node.metadata = NodeMetadata({ "tket.hint.max_qubits": 1, "preset-key": "preset-value", - } + }) + guppy_metadata = GuppyMetadata() - guppy_metadata.max_qubits.value = 5 + guppy_metadata.set_max_qubits(5) with pytest.raises( GuppyError, check=lambda e: ( @@ -81,10 +83,10 @@ def test_add_metadata_metadata_already_set(): def test_add_metadata_property_max_qubits(): mock_hugr_node = Mock() - mock_hugr_node.metadata = {} + mock_hugr_node.metadata = NodeMetadata({}) guppy_metadata = GuppyMetadata() - guppy_metadata.max_qubits.value = 5 + guppy_metadata.set_max_qubits(5) add_metadata(mock_hugr_node, guppy_metadata) - assert mock_hugr_node.metadata == {"tket.hint.max_qubits": 5} + assert mock_hugr_node.metadata.as_dict() == {"tket.hint.max_qubits": 5} diff --git a/tests/test_version_metadata.py b/tests/metadata/test_version_metadata.py similarity index 100% rename from tests/test_version_metadata.py rename to tests/metadata/test_version_metadata.py From ffa288ddb253ee5f44fef58b090deee605f65950 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Tue, 3 Mar 2026 15:39:00 +0000 Subject: [PATCH 04/17] Attach subprogram info --- .../src/guppylang_internals/compiler/core.py | 7 +++ .../definition/function.py | 15 ++++- .../src/guppylang_internals/engine.py | 34 +++++++---- .../guppylang_internals/metadata/common.py | 2 +- .../metadata/debug_info.py | 60 ++++++++++++------- tests/metadata/test_debug_info.py | 11 ++++ 6 files changed, 93 insertions(+), 36 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/compiler/core.py b/guppylang-internals/src/guppylang_internals/compiler/core.py index 4bc8eadaf..7fb4e3066 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/core.py +++ b/guppylang-internals/src/guppylang_internals/compiler/core.py @@ -40,6 +40,7 @@ from guppylang_internals.diagnostic import Error from guppylang_internals.engine import DEF_STORE, ENGINE from guppylang_internals.error import GuppyError, InternalGuppyError +from guppylang_internals.metadata.debug_info import StringTable from guppylang_internals.std._internal.compiler.tket_exts import GUPPY_EXTENSION from guppylang_internals.tys.arg import ConstArg, TypeArg from guppylang_internals.tys.builtin import nat_type @@ -151,9 +152,12 @@ class CompilerContext(ToHugrContext): checked_globals: Globals + metadata_file_table: StringTable + def __init__( self, module: DefinitionBuilder[ops.Module], + file_table: StringTable | None = None, ) -> None: self.module = module self.worklist = {} @@ -161,6 +165,9 @@ def __init__( self.global_funcs = {} self.checked_globals = Globals(None) self.current_mono_args = None + self.metadata_file_table = ( + file_table if file_table is not None else StringTable([]) + ) @contextmanager def set_monomorphized_args( diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index d16c4d3d0..1a681c481 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -10,7 +10,13 @@ from hugr.build.dfg import DefinitionBuilder, OpVar from hugr.hugr.node_port import ToNode -from guppylang_internals.ast_util import AstNode, annotate_location, with_loc, with_type +from guppylang_internals.ast_util import ( + AstNode, + annotate_location, + get_file, + with_loc, + with_type, +) from guppylang_internals.checker.cfg_checker import CheckedCFG from guppylang_internals.checker.core import Context, Globals, Place from guppylang_internals.checker.errors.generic import ExpectedError @@ -42,6 +48,7 @@ ) from guppylang_internals.error import GuppyError from guppylang_internals.metadata.common import GuppyMetadata, add_metadata +from guppylang_internals.metadata.debug_info import DISubprogram from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.tys.subst import Inst, Subst @@ -197,6 +204,12 @@ def monomorphize( func_def = module.module_root_builder().define_function( hugr_func_name, hugr_ty.body.input, hugr_ty.body.output, hugr_ty.params ) + subprogram = DISubprogram( + file=ctx.metadata_file_table.get_index(get_file(self.defined_at)), + line_no=self.defined_at.lineno, + scope_line=self.defined_at.body[0].lineno, + ) + self.metadata.set_debug_info(subprogram) add_metadata( func_def, self.metadata, diff --git a/guppylang-internals/src/guppylang_internals/engine.py b/guppylang-internals/src/guppylang_internals/engine.py index e980dc45c..c5b807a2d 100644 --- a/guppylang-internals/src/guppylang_internals/engine.py +++ b/guppylang-internals/src/guppylang_internals/engine.py @@ -28,7 +28,11 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import pretty_errors -from guppylang_internals.metadata.debug_info import DICompileUnit, HugrDebugInfo +from guppylang_internals.metadata.debug_info import ( + DICompileUnit, + HugrDebugInfo, + StringTable, +) from guppylang_internals.span import SourceMap from guppylang_internals.tracing.util import get_calling_frame from guppylang_internals.tys.builtin import ( @@ -283,22 +287,17 @@ def compile(self, id: DefId) -> ModulePointer: graph = hf.Module() graph.metadata["name"] = "__main__" # entrypoint metadata - # Add debug info about the module to the root node + # Lower definitions to Hugr + from guppylang_internals.compiler.core import CompilerContext + + # Set up string tables for metadata serialization. We know that the first entry + # in the table is always the file containing the Hugr entrypoint. frame = get_calling_frame() assert frame is not None filename = frame.f_code.co_filename - module_info = DICompileUnit( - directory=Path.cwd().as_uri(), - # We know this file is always the first entry in the file table. - filename=0, - file_table=[filename], - ) - graph.hugr.module_root.metadata[HugrDebugInfo] = module_info + file_table = StringTable([filename]) - # Lower definitions to Hugr - from guppylang_internals.compiler.core import CompilerContext - - ctx = CompilerContext(graph) + ctx = CompilerContext(graph, file_table) compiled_def = ctx.compile(self.checked[id]) self.compiled = ctx.compiled @@ -311,6 +310,15 @@ def compile(self, id: DefId) -> ModulePointer: # loosened after https://github.com/quantinuum/hugr/issues/2501 is fixed graph.hugr.entrypoint = compiled_def.hugr_node + # Add debug info about the module to the root node + module_info = DICompileUnit( + directory=Path.cwd().as_uri(), + # We know this file is always the first entry in the file table. + filename=ctx.metadata_file_table.get_index(filename), + file_table=ctx.metadata_file_table.table, + ) + graph.hugr.module_root.metadata[HugrDebugInfo] = module_info + # Use cached base extensions and registry, only add additional extensions base_extensions = self._get_base_packaged_extensions() packaged_extensions = [*base_extensions, *self.additional_extensions] diff --git a/guppylang-internals/src/guppylang_internals/metadata/common.py b/guppylang-internals/src/guppylang_internals/metadata/common.py index 431a90067..e7a398b05 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/common.py +++ b/guppylang-internals/src/guppylang_internals/metadata/common.py @@ -41,7 +41,7 @@ def as_dict(self) -> dict[str, JsonType]: return self._node_metadata def set_debug_info(self, debug_info: DebugRecord) -> None: - self._node_metadata[HugrDebugInfo.KEY] = debug_info + self._node_metadata[HugrDebugInfo.KEY] = debug_info.to_json() def set_max_qubits(self, max_qubits: int) -> None: self._node_metadata[MetadataMaxQubits.KEY] = max_qubits diff --git a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py index 27114a83b..aac995aaa 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py +++ b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py @@ -69,18 +69,6 @@ def from_json(cls, value: JsonType) -> "DICompileUnit": file_table=list[str](value["file_table"]), ) - def get_file_index(self, filename: str) -> int: - for idx, name in enumerate(self.file_table): - if name == filename: - return idx - else: - idx = len(self.file_table) - self.file_table.append(filename) - return idx - - def get_filename(self, idx: int) -> str: - return self.file_table[idx] - @dataclass class DISubprogram(DebugRecord): @@ -89,28 +77,37 @@ class DISubprogram(DebugRecord): file: int # Index into the string table for filenames. line_no: int # First line of the function definition. - scope_line: int # First line of the function body. + scope_line: int | None # First line of the function body. def to_json(self) -> dict[str, str]: - return { - "file": str(self.file), - "line_no": str(self.line_no), - "scope_line": str(self.scope_line), - } + return ( + { + "file": str(self.file), + "line_no": str(self.line_no), + "scope_line": str(self.scope_line), + } + if self.scope_line is not None + else { + "file": str(self.file), + "line_no": str(self.line_no), + } + ) @classmethod def from_json(cls, value: JsonType) -> "DISubprogram": if not isinstance(value, dict): msg = f"Expected a dictionary for DISubprogram, but got {type(value)}" raise TypeError(msg) - for key in ("file", "line_no", "scope_line"): + for key in ("file", "line_no"): if key not in value: msg = f"Expected DISubprogram to have a '{key}' key but got {value}" raise TypeError(msg) + # Declarations have no function body so no scope_line. + scope_line = int(value["scope_line"]) if "scope_line" in value else None return DISubprogram( file=int(value["file"]), line_no=int(value["line_no"]), - scope_line=int(value["scope_line"]), + scope_line=scope_line, ) @@ -125,7 +122,7 @@ class DILocation(DebugRecord): def to_json(self) -> dict[str, str]: return { "column": str(self.column), - "lline_noe": str(self.line_no), + "line_no": str(self.line_no), } @classmethod @@ -138,3 +135,24 @@ def from_json(cls, value: JsonType) -> "DILocation": msg = f"Expected DILocation to have a '{key}' key but got {value}" raise TypeError(msg) return DILocation(column=int(value["column"]), line_no=int(value["line"])) + + +@dataclass +class StringTable: + """Utility class for managing a string table for debug info serialization.""" + table: list[str] + + def get_index(self, s: str) -> int: + """Returns the index of `s` in the string table, adding it if it's not already + present.""" + for idx, entry in enumerate(self.table): + if entry == s: + return idx + else: + idx = len(self.table) + self.table.append(s) + return idx + + def get_string(self, idx: int) -> str: + """Returns the string corresponding to `idx` in the string table.""" + return self.table[idx] diff --git a/tests/metadata/test_debug_info.py b/tests/metadata/test_debug_info.py index 74c2224ca..c516c06cc 100644 --- a/tests/metadata/test_debug_info.py +++ b/tests/metadata/test_debug_info.py @@ -11,3 +11,14 @@ def foo() -> None: hugr = foo.compile().modules[0] meta = hugr.module_root.metadata assert HugrDebugInfo.KEY in meta + + +def test_subprogram(): + @guppy + def foo() -> None: + pass + + hugr = foo.compile().modules[0] + func = hugr.children(hugr.module_root)[0] + meta = func.metadata + assert HugrDebugInfo.KEY in meta From 791885dcd3812690d42e7ad1dc5926b454d90d93 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Wed, 4 Mar 2026 09:32:08 +0000 Subject: [PATCH 05/17] Add metadata to decls --- .../guppylang_internals/definition/declaration.py | 14 +++++++++++++- .../src/guppylang_internals/metadata/debug_info.py | 2 +- tests/metadata/test_debug_info.py | 12 +++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index 315e41dad..27c42a55e 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -2,11 +2,18 @@ from dataclasses import dataclass, field from typing import ClassVar +from guppylang_internals.metadata.debug_info import DISubprogram, HugrDebugInfo from hugr import Node, Wire from hugr.build import function as hf from hugr.build.dfg import DefinitionBuilder, OpVar -from guppylang_internals.ast_util import AstNode, has_empty_body, with_loc, with_type +from guppylang_internals.ast_util import ( + AstNode, + get_file, + has_empty_body, + with_loc, + with_type, +) from guppylang_internals.checker.core import Context, Globals from guppylang_internals.checker.expr_checker import check_call, synthesize_call from guppylang_internals.checker.func_checker import check_signature @@ -126,6 +133,11 @@ def compile_outer( module: hf.Module = module node = module.declare_function(self.name, self.ty.to_hugr_poly(ctx)) + subprogram = DISubprogram( + file=ctx.metadata_file_table.get_index(get_file(self.defined_at)), + line_no=self.defined_at.lineno, + ) + node.metadata[HugrDebugInfo] = subprogram return CompiledFunctionDecl( self.id, self.name, diff --git a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py index aac995aaa..55dd47483 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py +++ b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py @@ -77,7 +77,7 @@ class DISubprogram(DebugRecord): file: int # Index into the string table for filenames. line_no: int # First line of the function definition. - scope_line: int | None # First line of the function body. + scope_line: int | None = None # First line of the function body. def to_json(self) -> dict[str, str]: return ( diff --git a/tests/metadata/test_debug_info.py b/tests/metadata/test_debug_info.py index c516c06cc..2d647906e 100644 --- a/tests/metadata/test_debug_info.py +++ b/tests/metadata/test_debug_info.py @@ -13,7 +13,7 @@ def foo() -> None: assert HugrDebugInfo.KEY in meta -def test_subprogram(): +def test_subprogram_defn(): @guppy def foo() -> None: pass @@ -22,3 +22,13 @@ def foo() -> None: func = hugr.children(hugr.module_root)[0] meta = func.metadata assert HugrDebugInfo.KEY in meta + + +def test_subprogram_decl(): + @guppy.declare + def foo() -> None: ... + + hugr = foo.compile().modules[0] + func = hugr.children(hugr.module_root)[0] + meta = func.metadata + assert HugrDebugInfo.KEY in meta From dd9b68d97fe6b1608756a0b9e7406edab458e1f5 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Wed, 4 Mar 2026 11:13:11 +0000 Subject: [PATCH 06/17] Add location info to calls --- .../definition/declaration.py | 8 ++++---- .../guppylang_internals/definition/function.py | 17 +++++++++++++---- .../definition/pytket_circuits.py | 2 +- .../guppylang_internals/definition/traced.py | 5 +++++ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index 27c42a55e..7b725555f 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -2,7 +2,6 @@ from dataclasses import dataclass, field from typing import ClassVar -from guppylang_internals.metadata.debug_info import DISubprogram, HugrDebugInfo from hugr import Node, Wire from hugr.build import function as hf from hugr.build.dfg import DefinitionBuilder, OpVar @@ -37,6 +36,7 @@ ) from guppylang_internals.diagnostic import Error from guppylang_internals.error import GuppyError +from guppylang_internals.metadata.debug_info import DISubprogram, HugrDebugInfo from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.tys.param import Parameter @@ -133,11 +133,11 @@ def compile_outer( module: hf.Module = module node = module.declare_function(self.name, self.ty.to_hugr_poly(ctx)) - subprogram = DISubprogram( + metadata = DISubprogram( file=ctx.metadata_file_table.get_index(get_file(self.defined_at)), line_no=self.defined_at.lineno, ) - node.metadata[HugrDebugInfo] = subprogram + node.metadata[HugrDebugInfo] = metadata return CompiledFunctionDecl( self.id, self.name, @@ -183,4 +183,4 @@ def compile_call( ) -> CallReturnWires: """Compiles a call to the function.""" # Use implementation from function definition. - return compile_call(args, type_args, dfg, self.ty, self.declaration) + return compile_call(args, type_args, dfg, self.ty, self.declaration, node) diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 1a681c481..412b6e098 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -48,7 +48,11 @@ ) from guppylang_internals.error import GuppyError from guppylang_internals.metadata.common import GuppyMetadata, add_metadata -from guppylang_internals.metadata.debug_info import DISubprogram +from guppylang_internals.metadata.debug_info import ( + DILocation, + DISubprogram, + HugrDebugInfo, +) from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.tys.subst import Inst, Subst @@ -204,12 +208,12 @@ def monomorphize( func_def = module.module_root_builder().define_function( hugr_func_name, hugr_ty.body.input, hugr_ty.body.output, hugr_ty.params ) - subprogram = DISubprogram( + metadata = DISubprogram( file=ctx.metadata_file_table.get_index(get_file(self.defined_at)), line_no=self.defined_at.lineno, scope_line=self.defined_at.body[0].lineno, ) - self.metadata.set_debug_info(subprogram) + self.metadata.set_debug_info(metadata) add_metadata( func_def, self.metadata, @@ -272,7 +276,7 @@ def compile_call( node: AstNode, ) -> CallReturnWires: """Compiles a call to the function.""" - return compile_call(args, type_args, dfg, self.ty, self.func_def) + return compile_call(args, type_args, dfg, self.ty, self.func_def, node) def compile_inner(self, globals: CompilerContext) -> None: """Compiles the body of the function.""" @@ -297,12 +301,17 @@ def compile_call( dfg: DFContainer, ty: FunctionType, func: ToNode, + call_ast: AstNode, ) -> CallReturnWires: """Compiles a call to the function.""" func_ty: ht.FunctionType = ty.instantiate(type_args).to_hugr(dfg.ctx) type_args = [arg.to_hugr(dfg.ctx) for arg in type_args] num_returns = len(type_to_row(ty.output)) call = dfg.builder.call(func, *args, instantiation=func_ty, type_args=type_args) + call.metadata[HugrDebugInfo] = DILocation( + line_no=call_ast.lineno, + column=call_ast.col_offset, + ) return CallReturnWires( regular_returns=list(call[:num_returns]), inout_returns=list(call[num_returns:]), diff --git a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py index 9c6e3f8ad..9d5a81b9c 100644 --- a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py +++ b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py @@ -364,7 +364,7 @@ def compile_call( ) -> CallReturnWires: """Compiles a call to the function.""" # Use implementation from function definition. - return compile_call(args, type_args, dfg, self.ty, self.func_def) + return compile_call(args, type_args, dfg, self.ty, self.func_def, node) def _signature_from_circuit( diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index 002405682..f56cdfcf6 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -31,6 +31,7 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import GuppyError +from guppylang_internals.metadata.debug_info import DILocation, HugrDebugInfo from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.tys.subst import Inst, Subst @@ -139,6 +140,10 @@ def compile_call( call = dfg.builder.call( self.func_def, *args, instantiation=func_ty, type_args=type_args ) + call.metadata[HugrDebugInfo] = DILocation( + line_no=node.lineno, + column=node.col_offset, + ) return CallReturnWires( regular_returns=list(call[:num_returns]), inout_returns=list(call[num_returns:]), From ffbcf54244e42e15629babab509a216439f06c58 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Wed, 4 Mar 2026 15:23:37 +0000 Subject: [PATCH 07/17] Add proper tests + fix line_no bug --- .../definition/declaration.py | 4 +- .../definition/function.py | 11 +-- .../guppylang_internals/definition/traced.py | 7 +- .../metadata/debug_info.py | 8 +- tests/metadata/test_debug_info.py | 75 +++++++++++++++---- tests/resources/metadata_example.py | 14 ++++ 6 files changed, 93 insertions(+), 26 deletions(-) create mode 100644 tests/resources/metadata_example.py diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index 7b725555f..e5c5424ef 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -38,7 +38,7 @@ from guppylang_internals.error import GuppyError from guppylang_internals.metadata.debug_info import DISubprogram, HugrDebugInfo from guppylang_internals.nodes import GlobalCall -from guppylang_internals.span import SourceMap +from guppylang_internals.span import SourceMap, to_span from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.subst import Inst, Subst from guppylang_internals.tys.ty import Type, UnitaryFlags @@ -135,7 +135,7 @@ def compile_outer( node = module.declare_function(self.name, self.ty.to_hugr_poly(ctx)) metadata = DISubprogram( file=ctx.metadata_file_table.get_index(get_file(self.defined_at)), - line_no=self.defined_at.lineno, + line_no=to_span(self.defined_at).start.line, ) node.metadata[HugrDebugInfo] = metadata return CompiledFunctionDecl( diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 412b6e098..8ba2e67e3 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -54,7 +54,7 @@ HugrDebugInfo, ) from guppylang_internals.nodes import GlobalCall -from guppylang_internals.span import SourceMap +from guppylang_internals.span import SourceMap, to_span from guppylang_internals.tys.subst import Inst, Subst from guppylang_internals.tys.ty import FunctionType, Type, UnitaryFlags, type_to_row @@ -210,8 +210,8 @@ def monomorphize( ) metadata = DISubprogram( file=ctx.metadata_file_table.get_index(get_file(self.defined_at)), - line_no=self.defined_at.lineno, - scope_line=self.defined_at.body[0].lineno, + line_no=to_span(self.defined_at).start.line, + scope_line=to_span(self.defined_at.body[0]).start.line, ) self.metadata.set_debug_info(metadata) add_metadata( @@ -308,9 +308,10 @@ def compile_call( type_args = [arg.to_hugr(dfg.ctx) for arg in type_args] num_returns = len(type_to_row(ty.output)) call = dfg.builder.call(func, *args, instantiation=func_ty, type_args=type_args) + loc = to_span(call_ast).start call.metadata[HugrDebugInfo] = DILocation( - line_no=call_ast.lineno, - column=call_ast.col_offset, + line_no=loc.line, + column=loc.column, ) return CallReturnWires( regular_returns=list(call[:num_returns]), diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index f56cdfcf6..2fbd456c6 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -33,7 +33,7 @@ from guppylang_internals.error import GuppyError from guppylang_internals.metadata.debug_info import DILocation, HugrDebugInfo from guppylang_internals.nodes import GlobalCall -from guppylang_internals.span import SourceMap +from guppylang_internals.span import SourceMap, to_span from guppylang_internals.tys.subst import Inst, Subst from guppylang_internals.tys.ty import FunctionType, Type, type_to_row @@ -140,9 +140,10 @@ def compile_call( call = dfg.builder.call( self.func_def, *args, instantiation=func_ty, type_args=type_args ) + loc = to_span(node).start call.metadata[HugrDebugInfo] = DILocation( - line_no=node.lineno, - column=node.col_offset, + line_no=loc.line, + column=loc.column, ) return CallReturnWires( regular_returns=list(call[:num_returns]), diff --git a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py index 55dd47483..381a20fb3 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py +++ b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py @@ -77,7 +77,7 @@ class DISubprogram(DebugRecord): file: int # Index into the string table for filenames. line_no: int # First line of the function definition. - scope_line: int | None = None # First line of the function body. + scope_line: int | None = None # First line of the function body. def to_json(self) -> dict[str, str]: return ( @@ -86,6 +86,7 @@ def to_json(self) -> dict[str, str]: "line_no": str(self.line_no), "scope_line": str(self.scope_line), } + # Declarations have no function body so could have no scope_line. if self.scope_line is not None else { "file": str(self.file), @@ -102,7 +103,7 @@ def from_json(cls, value: JsonType) -> "DISubprogram": if key not in value: msg = f"Expected DISubprogram to have a '{key}' key but got {value}" raise TypeError(msg) - # Declarations have no function body so no scope_line. + # Declarations have no function body so could have no scope_line. scope_line = int(value["scope_line"]) if "scope_line" in value else None return DISubprogram( file=int(value["file"]), @@ -134,12 +135,13 @@ def from_json(cls, value: JsonType) -> "DILocation": if key not in value: msg = f"Expected DILocation to have a '{key}' key but got {value}" raise TypeError(msg) - return DILocation(column=int(value["column"]), line_no=int(value["line"])) + return DILocation(column=int(value["column"]), line_no=int(value["line_no"])) @dataclass class StringTable: """Utility class for managing a string table for debug info serialization.""" + table: list[str] def get_index(self, s: str) -> int: diff --git a/tests/metadata/test_debug_info.py b/tests/metadata/test_debug_info.py index 2d647906e..454892ab1 100644 --- a/tests/metadata/test_debug_info.py +++ b/tests/metadata/test_debug_info.py @@ -1,6 +1,17 @@ -from guppylang_internals.metadata.debug_info import HugrDebugInfo +from guppylang_internals.metadata.debug_info import ( + DICompileUnit, + DILocation, + DISubprogram, + HugrDebugInfo, +) +from hugr.ops import Call, FuncDecl, FuncDefn from guppylang import guppy +from tests.resources.metadata_example import bar, baz + + +def get_last_uri_part(uri: str) -> str: + return uri.split("/")[-1] def test_compile_unit(): @@ -10,25 +21,63 @@ def foo() -> None: hugr = foo.compile().modules[0] meta = hugr.module_root.metadata - assert HugrDebugInfo.KEY in meta + assert HugrDebugInfo in meta + debug_info = DICompileUnit.from_json(meta[HugrDebugInfo.KEY]) + assert get_last_uri_part(debug_info.directory) == "guppylang" + assert get_last_uri_part(debug_info.file_table[0]) == "test_debug_info.py" + assert debug_info.filename == 0 -def test_subprogram_defn(): +def test_subprogram(): @guppy def foo() -> None: - pass + bar() + baz() hugr = foo.compile().modules[0] - func = hugr.children(hugr.module_root)[0] - meta = func.metadata - assert HugrDebugInfo.KEY in meta + meta = hugr.module_root.metadata + assert HugrDebugInfo in meta + debug_info = DICompileUnit.from_json(meta[HugrDebugInfo.KEY]) + assert [get_last_uri_part(uri) for uri in debug_info.file_table] == [ + "test_debug_info.py", + "metadata_example.py", + ] + funcs = hugr.children(hugr.module_root) + for func in funcs: + op = hugr[func].op + assert isinstance(op, FuncDefn | FuncDecl) + match op.f_name: + case "foo": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 0 + assert debug_info.line_no == 33 + assert debug_info.scope_line == 34 + case "bar": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 7 + assert debug_info.scope_line == 10 + case "baz": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 14 + assert debug_info.scope_line is None + case _: + raise AssertionError(f"Unexpected function name {op.f_name}") -def test_subprogram_decl(): - @guppy.declare - def foo() -> None: ... +def test_call_location(): + @guppy + def foo() -> None: + bar() hugr = foo.compile().modules[0] - func = hugr.children(hugr.module_root)[0] - meta = func.metadata - assert HugrDebugInfo.KEY in meta + for node, node_data in hugr.nodes(): + if isinstance(node_data.op, Call): + assert HugrDebugInfo in node.metadata + debug_info = DILocation.from_json(node.metadata[HugrDebugInfo.KEY]) + assert debug_info.line_no == 75 + assert debug_info.column == 8 diff --git a/tests/resources/metadata_example.py b/tests/resources/metadata_example.py new file mode 100644 index 000000000..1b89ba117 --- /dev/null +++ b/tests/resources/metadata_example.py @@ -0,0 +1,14 @@ +"""File used to test the filename table in debug info metadata.""" + +from guppylang import guppy + + +@guppy +def bar() -> None: + # Leave white space to check scope_line is set correctly. + + pass + + +@guppy.declare +def baz() -> None: ... From 79202f37608426d29338d5a55db00414e9ebcc78 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Wed, 4 Mar 2026 16:38:58 +0000 Subject: [PATCH 08/17] Add location info in custom func calls --- .../src/guppylang_internals/definition/custom.py | 15 ++++++++++++++- .../guppylang_internals/definition/function.py | 7 ++----- .../src/guppylang_internals/definition/traced.py | 8 ++------ .../guppylang_internals/metadata/debug_info.py | 8 ++++++++ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/custom.py b/guppylang-internals/src/guppylang_internals/definition/custom.py index 89380d1d5..b3e2795ab 100644 --- a/guppylang-internals/src/guppylang_internals/definition/custom.py +++ b/guppylang-internals/src/guppylang_internals/definition/custom.py @@ -29,6 +29,11 @@ from guppylang_internals.definition.value import CallReturnWires, CompiledCallableDef from guppylang_internals.diagnostic import Error, Help from guppylang_internals.error import GuppyError, InternalGuppyError +from guppylang_internals.metadata.debug_info import ( + DILocation, + HugrDebugInfo, + make_location_record, +) from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.std._internal.compiler.tket_bool import ( @@ -297,7 +302,15 @@ def compile_call( hugr_ty = concrete_ty.to_hugr(ctx) self.call_compiler._setup(type_args, dfg, ctx, node, hugr_ty, self) - return self.call_compiler.compile_with_inouts(args) + wires = self.call_compiler.compile_with_inouts(args) + if wires.regular_returns: + # If the call returns something, we can find the op node which produced the + # return values by following the wire of one of the returns and attaching + # debug info to the parent we found. + wires.regular_returns[0].out_port().node.metadata[HugrDebugInfo] = ( + make_location_record(node) + ) + return wires class CustomCallChecker(ABC): diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 8ba2e67e3..484623667 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -52,6 +52,7 @@ DILocation, DISubprogram, HugrDebugInfo, + make_location_record, ) from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap, to_span @@ -308,11 +309,7 @@ def compile_call( type_args = [arg.to_hugr(dfg.ctx) for arg in type_args] num_returns = len(type_to_row(ty.output)) call = dfg.builder.call(func, *args, instantiation=func_ty, type_args=type_args) - loc = to_span(call_ast).start - call.metadata[HugrDebugInfo] = DILocation( - line_no=loc.line, - column=loc.column, - ) + call.metadata[HugrDebugInfo] = make_location_record(call_ast) return CallReturnWires( regular_returns=list(call[:num_returns]), inout_returns=list(call[num_returns:]), diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index 2fbd456c6..5170af7fb 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -31,7 +31,7 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import GuppyError -from guppylang_internals.metadata.debug_info import DILocation, HugrDebugInfo +from guppylang_internals.metadata.debug_info import DILocation, HugrDebugInfo, make_location_record from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap, to_span from guppylang_internals.tys.subst import Inst, Subst @@ -140,11 +140,7 @@ def compile_call( call = dfg.builder.call( self.func_def, *args, instantiation=func_ty, type_args=type_args ) - loc = to_span(node).start - call.metadata[HugrDebugInfo] = DILocation( - line_no=loc.line, - column=loc.column, - ) + call.metadata[HugrDebugInfo] = make_location_record(node) return CallReturnWires( regular_returns=list(call[:num_returns]), inout_returns=list(call[num_returns:]), diff --git a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py index 381a20fb3..baefa129a 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py +++ b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass +from guppylang_internals.span import ToSpan, to_span from hugr.metadata import JsonType, Metadata @@ -138,6 +139,13 @@ def from_json(cls, value: JsonType) -> "DILocation": return DILocation(column=int(value["column"]), line_no=int(value["line_no"])) +def make_location_record(node: ToSpan) -> DILocation: + """Creates a DILocation metadata record for `node`.""" + return DILocation( + line_no=to_span(node).start.line, column=to_span(node).start.column + ) + + @dataclass class StringTable: """Utility class for managing a string table for debug info serialization.""" From 29c69a7548301a308af5068b81eefefe3d1b1054 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Thu, 5 Mar 2026 15:19:46 +0000 Subject: [PATCH 09/17] Attach location data to (most) extension ops --- .../compiler/expr_compiler.py | 118 ++++++++++++------ .../guppylang_internals/definition/custom.py | 40 +++--- .../definition/declaration.py | 12 +- .../definition/function.py | 38 ++++-- .../guppylang_internals/definition/struct.py | 2 +- .../guppylang_internals/metadata/common.py | 12 +- .../metadata/debug_info.py | 6 +- .../std/_internal/compiler/array.py | 30 ++--- .../std/_internal/compiler/either.py | 9 +- .../std/_internal/compiler/list.py | 34 ++--- .../std/_internal/compiler/option.py | 2 +- .../std/_internal/compiler/platform.py | 12 +- .../std/_internal/compiler/prelude.py | 4 +- guppylang/src/guppylang/decorator.py | 9 +- tests/metadata/test_debug_info.py | 24 +++- tests/metadata/test_metadata.py | 21 ++-- 16 files changed, 229 insertions(+), 144 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py index bf02898c3..d9357120c 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py +++ b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py @@ -9,14 +9,14 @@ import hugr.std.int import hugr.std.logic import hugr.std.prelude -from hugr import Wire, ops +from hugr import Node, Wire, ops from hugr import tys as ht from hugr import val as hv from hugr.build import function as hf from hugr.build.cond_loop import Conditional from hugr.build.dfg import DP, DfBase -from guppylang_internals.ast_util import AstNode, AstVisitor, get_type +from guppylang_internals.ast_util import AstNode, AstVisitor, get_file, get_type from guppylang_internals.cfg.builder import tmp_vars from guppylang_internals.checker.core import Variable, contains_subscript from guppylang_internals.checker.errors.generic import UnsupportedError @@ -37,6 +37,7 @@ ) from guppylang_internals.engine import ENGINE from guppylang_internals.error import GuppyError, InternalGuppyError +from guppylang_internals.metadata.debug_info import HugrDebugInfo, make_location_record from guppylang_internals.nodes import ( AbortExpr, AbortKind, @@ -129,6 +130,12 @@ def compile_row(self, expr: ast.expr, dfg: DFContainer) -> list[Wire]: """ return [self.compile(e, dfg) for e in expr_to_row(expr)] + def add_op( + self, op: ops.DataflowOp, /, *args: Wire, ast_node: AstNode | None = None + ) -> Node: + """Adds an op to the builder, with optional debug info.""" + return add_op(self.builder, op, *args, ast_node=ast_node) + @property def builder(self) -> DfBase[ops.DfParentOp]: """The current Hugr dataflow graph builder.""" @@ -214,7 +221,7 @@ def _if_else( cond_wire = self.visit(cond) cond_ty = self.builder.hugr.port_type(cond_wire.out_port()) if cond_ty == OpaqueBool: - cond_wire = self.builder.add_op(read_bool(), cond_wire) + cond_wire = self.add_op(read_bool(), cond_wire) conditional = self.builder.add_conditional( cond_wire, *(self.visit(inp) for inp in inputs) ) @@ -286,8 +293,8 @@ def visit_GenericParamValue(self, node: GenericParamValue) -> Wire: load_nat = hugr.std.PRELUDE.get_op("load_nat").instantiate( [arg], ht.FunctionType([], [ht.USize()]) ) - usize = self.builder.add_op(load_nat) - return self.builder.add_op(convert_ifromusize(), usize) + usize = self.add_op(load_nat, ast_node=node) + return self.add_op(convert_ifromusize(), usize, ast_node=node) case ty: # Look up monomorphization match self.ctx.current_mono_args[node.param.idx]: @@ -316,12 +323,12 @@ def visit_List(self, node: ast.List) -> Wire: def _unpack_tuple(self, wire: Wire, types: Sequence[Type]) -> Sequence[Wire]: """Add a tuple unpack operation to the graph""" types = [t.to_hugr(self.ctx) for t in types] - return list(self.builder.add_op(ops.UnpackTuple(types), wire)) + return list(self.add_op(ops.UnpackTuple(types), wire)) def _pack_tuple(self, wires: Sequence[Wire], types: Sequence[Type]) -> Wire: """Add a tuple pack operation to the graph""" types = [t.to_hugr(self.ctx) for t in types] - return self.builder.add_op(ops.MakeTuple(types), *wires) + return self.add_op(ops.MakeTuple(types), *wires) def _pack_returns(self, returns: Sequence[Wire], return_ty: Type) -> Wire: """Groups function return values into a tuple""" @@ -363,8 +370,8 @@ def visit_LocalCall(self, node: LocalCall) -> Wire: num_returns = len(type_to_row(func_ty.output)) args = self._compile_call_args(node.args, func_ty) - call = self.builder.add_op( - ops.CallIndirect(func_ty.to_hugr(self.ctx)), func, *args + call = self.add_op( + ops.CallIndirect(func_ty.to_hugr(self.ctx)), func, *args, ast_node=node ) regular_returns = list(call[:num_returns]) inout_returns = call[num_returns:] @@ -420,7 +427,7 @@ def _compile_tensor_with_leftovers( num_returns = len(type_to_row(func_ty.output)) consumed_args, other_args = args[0:input_len], args[input_len:] consumed_wires = self._compile_call_args(consumed_args, func_ty) - call = self.builder.add_op( + call = self.add_op( ops.CallIndirect(func_ty.to_hugr(self.ctx)), func, *consumed_wires ) regular_returns: list[Wire] = list(call[:num_returns]) @@ -472,8 +479,11 @@ def visit_PartialApply(self, node: PartialApply) -> Wire: func_ty.to_hugr(self.ctx), [get_type(arg).to_hugr(self.ctx) for arg in node.args], ) - return self.builder.add_op( - op, self.visit(node.func), *(self.visit(arg) for arg in node.args) + return self.add_op( + op, + self.visit(node.func), + *(self.visit(arg) for arg in node.args), + ast_node=node, ) def visit_TypeApply(self, node: TypeApply) -> Wire: @@ -503,7 +513,7 @@ def visit_UnaryOp(self, node: ast.UnaryOp) -> Wire: # since it is not implemented via a dunder method if isinstance(node.op, ast.Not): arg = self.visit(node.operand) - return self.builder.add_op(not_op(), arg) + return self.add_op(not_op(), arg, ast_node=node) raise InternalGuppyError("Node should have been removed during type checking.") @@ -561,9 +571,9 @@ def _visit_result_tag(self, tag: Const, loc: ast.expr) -> str: def visit_AbortExpr(self, node: AbortExpr) -> Wire: signal = self.visit(node.signal) - signal_usize = self.builder.add_op(convert_itousize(), signal) + signal_usize = self.add_op(convert_itousize(), signal, ast_node=node) msg = self.visit(node.msg) - err = self.builder.add_op(make_error(), signal_usize, msg) + err = self.add_op(make_error(), signal_usize, msg, ast_node=node) in_tys = [get_type(e).to_hugr(self.ctx) for e in node.values] out_tys = [ty.to_hugr(self.ctx) for ty in type_to_row(get_type(node))] args = [self.visit(e) for e in node.values] @@ -572,7 +582,7 @@ def visit_AbortExpr(self, node: AbortExpr) -> Wire: h_node = build_panic(self.builder, in_tys, out_tys, err, *args) case AbortKind.ExitShot: op = panic(in_tys, out_tys, AbortKind.ExitShot) - h_node = self.builder.add_op(op, err, *args) + h_node = self.add_op(op, err, *args, ast_node=node) return self._pack_returns(list(h_node.outputs()), get_type(node)) def visit_BarrierExpr(self, node: BarrierExpr) -> Wire: @@ -582,7 +592,7 @@ def visit_BarrierExpr(self, node: BarrierExpr) -> Wire: ht.FunctionType.endo(hugr_tys), ) - barrier_n = self.builder.add_op(op, *(self.visit(e) for e in node.args)) + barrier_n = self.add_op(op, *(self.visit(e) for e in node.args), ast_node=node) self._update_inout_ports(node.args, iter(barrier_n), node.func_ty) return self._pack_returns([], NoneType()) @@ -605,18 +615,22 @@ def visit_StateResultExpr(self, node: StateResultExpr) -> Wire: if not node.array_len: # If the input is a sequence of qubits, we pack them into an array. qubits_in = [self.visit(e) for e in node.args[1:]] - qubit_arr_in = self.builder.add_op( - array_new(ht.Qubit, len(node.args) - 1), *qubits_in + qubit_arr_in = self.add_op( + array_new(ht.Qubit, len(node.args) - 1), *qubits_in, ast_node=node ) # Turn into standard array from borrow array. - qubit_arr_in = self.builder.add_op( - array_to_std_array(ht.Qubit, num_qubits_arg), qubit_arr_in + qubit_arr_in = self.add_op( + array_to_std_array(ht.Qubit, num_qubits_arg), + qubit_arr_in, + ast_node=node, ) - qubit_arr_out = self.builder.add_op(op, qubit_arr_in) + qubit_arr_out = self.add_op(op, qubit_arr_in, ast_node=node) - qubit_arr_out = self.builder.add_op( - std_array_to_array(ht.Qubit, num_qubits_arg), qubit_arr_out + qubit_arr_out = self.add_op( + std_array_to_array(ht.Qubit, num_qubits_arg), + qubit_arr_out, + ast_node=node, ) qubits_out = unpack_array(self.builder, qubit_arr_out) else: @@ -655,8 +669,10 @@ def visit_DesugaredArrayComp(self, node: DesugaredArrayComp) -> Wire: count_var = Variable(next(tmp_vars), int_type(), node) hugr_elt_ty = node.elt_ty.to_hugr(self.ctx) # Initialise empty array. - self.dfg[array_var] = self.builder.add_op( - barray_new_all_borrowed(hugr_elt_ty, node.length.to_arg().to_hugr(self.ctx)) + self.dfg[array_var] = self.add_op( + barray_new_all_borrowed( + hugr_elt_ty, node.length.to_arg().to_hugr(self.ctx) + ), ) self.dfg[count_var] = self.builder.load( hugr.std.int.IntVal(0, width=NumericType.INT_WIDTH) @@ -664,7 +680,7 @@ def visit_DesugaredArrayComp(self, node: DesugaredArrayComp) -> Wire: with self._build_generators([node.generator], [array_var, count_var]): elt = self.visit(node.elt) array, count = self.dfg[array_var], self.dfg[count_var] - idx = self.builder.add_op(convert_itousize(), count) + idx = self.add_op(convert_itousize(), count) self.dfg[array_var] = self.builder.add_op( barray_return(hugr_elt_ty, node.length.to_arg().to_hugr(self.ctx)), array, @@ -748,6 +764,20 @@ def visit_Compare(self, node: ast.Compare) -> Wire: raise InternalGuppyError("Node should have been removed during type checking.") +def add_op( + builder: DfBase[ops.DfParentOp], + op: ops.DataflowOp, + /, + *args: Wire, + ast_node: AstNode | None = None, +) -> Node: + """Adds an op to the builder, with optional debug info.""" + op_node = builder.add_op(op, *args) + if ast_node is not None and get_file(ast_node) is not None: + op_node.metadata[HugrDebugInfo] = make_location_record(ast_node) + return op_node + + def expr_to_row(expr: ast.expr) -> list[ast.expr]: """Turns an expression into a row expressions by unpacking top-level tuples.""" return expr.elts if isinstance(expr, ast.Tuple) else [expr] @@ -758,13 +788,14 @@ def pack_returns( return_ty: Type, builder: DfBase[ops.DfParentOp], ctx: CompilerContext, + ast_node: AstNode | None = None, ) -> Wire: """Groups function return values into a tuple""" if isinstance(return_ty, TupleType | NoneType) and not return_ty.preserve: types = type_to_row(return_ty) assert len(returns) == len(types) hugr_tys = [t.to_hugr(ctx) for t in types] - return builder.add_op(ops.MakeTuple(hugr_tys), *returns) + return add_op(builder, ops.MakeTuple(hugr_tys), *returns, ast_node=ast_node) assert len(returns) == 1, ( f"Expected a single return value. Got {returns}. return type {return_ty}" ) @@ -772,13 +803,21 @@ def pack_returns( def unpack_wire( - wire: Wire, return_ty: Type, builder: DfBase[ops.DfParentOp], ctx: CompilerContext + wire: Wire, + return_ty: Type, + builder: DfBase[ops.DfParentOp], + ctx: CompilerContext, + ast_node: AstNode | None = None, ) -> list[Wire]: """The inverse of `pack_returns`""" if isinstance(return_ty, TupleType | NoneType) and not return_ty.preserve: types = type_to_row(return_ty) hugr_tys = [t.to_hugr(ctx) for t in types] - return list(builder.add_op(ops.UnpackTuple(hugr_tys), wire).outputs()) + return list( + add_op( + builder, ops.UnpackTuple(hugr_tys), wire, ast_node=ast_node + ).outputs() + ) return [wire] @@ -885,6 +924,7 @@ def apply_array_op_with_conversions( size_arg: ht.TypeArg, input_array: Wire, convert_bool: bool = False, + ast_node: AstNode | None = None, ) -> Wire: """Applies common transformations to a Guppy array input before it can be passed to a Hugr op operating on a standard Hugr array, and then reverses them again on the @@ -898,20 +938,28 @@ def apply_array_op_with_conversions( array_read = array_read_bool(ctx) array_read = builder.load_function(array_read) map_op = array_map(OpaqueBool, size_arg, ht.Bool) - input_array = builder.add_op(map_op, input_array, array_read) + input_array = add_op( + builder, map_op, input_array, array_read, ast_node=ast_node + ) elem_ty = ht.Bool - input_array = builder.add_op(array_to_std_array(elem_ty, size_arg), input_array) + input_array = add_op( + builder, array_to_std_array(elem_ty, size_arg), input_array, ast_node=ast_node + ) - result_array = builder.add_op(op, input_array) + result_array = add_op(builder, op, input_array, ast_node=ast_node) - result_array = builder.add_op(std_array_to_array(elem_ty, size_arg), result_array) + result_array = add_op( + builder, std_array_to_array(elem_ty, size_arg), result_array, ast_node=ast_node + ) if convert_bool: array_make_opaque = array_make_opaque_bool(ctx) array_make_opaque = builder.load_function(array_make_opaque) map_op = array_map(ht.Bool, size_arg, OpaqueBool) - result_array = builder.add_op(map_op, result_array, array_make_opaque) + result_array = add_op( + builder, map_op, result_array, array_make_opaque, ast_node=ast_node + ) elem_ty = OpaqueBool return result_array diff --git a/guppylang-internals/src/guppylang_internals/definition/custom.py b/guppylang-internals/src/guppylang_internals/definition/custom.py index b3e2795ab..d9238b0c6 100644 --- a/guppylang-internals/src/guppylang_internals/definition/custom.py +++ b/guppylang-internals/src/guppylang_internals/definition/custom.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, ClassVar -from hugr import Wire, ops +from hugr import Node, Wire, ops from hugr import tys as ht from hugr.build.dfg import DfBase from hugr.std.collections.borrow_array import EXTENSION as BORROW_ARRAY_EXTENSION @@ -29,11 +29,6 @@ from guppylang_internals.definition.value import CallReturnWires, CompiledCallableDef from guppylang_internals.diagnostic import Error, Help from guppylang_internals.error import GuppyError, InternalGuppyError -from guppylang_internals.metadata.debug_info import ( - DILocation, - HugrDebugInfo, - make_location_record, -) from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.std._internal.compiler.tket_bool import ( @@ -302,15 +297,7 @@ def compile_call( hugr_ty = concrete_ty.to_hugr(ctx) self.call_compiler._setup(type_args, dfg, ctx, node, hugr_ty, self) - wires = self.call_compiler.compile_with_inouts(args) - if wires.regular_returns: - # If the call returns something, we can find the op node which produced the - # return values by following the wire of one of the returns and attaching - # debug info to the parent we found. - wires.regular_returns[0].out_port().node.metadata[HugrDebugInfo] = ( - make_location_record(node) - ) - return wires + return self.call_compiler.compile_with_inouts(args) class CustomCallChecker(ABC): @@ -391,6 +378,13 @@ def builder(self) -> DfBase[ops.DfParentOp]: """The hugr dataflow builder.""" return self.dfg.builder + def add_op(self, op: ops.DataflowOp, *args: Wire) -> Node: + """Adds an op to the current builder ensuring debug information is added if + available.""" + from guppylang_internals.compiler.expr_compiler import add_op + + return add_op(self.builder, op, *args, ast_node=self.node) + class CustomCallCompiler(CustomInoutCallCompiler, ABC): """Abstract base class for custom function call compilers with only owned args.""" @@ -399,7 +393,11 @@ class CustomCallCompiler(CustomInoutCallCompiler, ABC): def compile(self, args: list[Wire]) -> list[Wire]: """Compiles a custom function call and returns the resulting ports. - Use the provided `self.builder` to add nodes to the Hugr graph. + Use `self.add_op` to add nodes to the Hugr graph. + + If you want to add a different builder than `self.builder`while still ensuring + debug information is attached during compilation, import `add_op` from + `expr_compiler` and pass `self.node` to it. """ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: @@ -449,7 +447,7 @@ def __init__( def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: op = self.op(self.ty, self.type_args, self.ctx) - node = self.builder.add_op(op, *args) + node = self.add_op(op, *args) num_returns = ( len(type_to_row(self.func.ty.output)) if self.func else len(self.ty.output) ) @@ -484,15 +482,15 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: hugr_op_ty = ht.FunctionType(converted_in, converted_out) op = self.op(hugr_op_ty, self.type_args, self.ctx) converted_args = [ - self.builder.add_op(read_bool(), arg) + self.add_op(read_bool(), arg) if self.builder.hugr.port_type(arg.out_port()) == OpaqueBool else arg for arg in args ] - node = self.builder.add_op(op, *converted_args) + node = self.add_op(op, *converted_args) result = list(node.outputs()) converted_result = [ - self.builder.add_op(make_opaque(), res) + self.add_op(make_opaque(), res) if self.builder.hugr.port_type(res.out_port()) == ht.Bool else res for res in result @@ -541,7 +539,7 @@ def _handle_affine_type(self, ty: ht.Type, arg: Wire) -> list[Wire]: type_args, ht.FunctionType(self.ty.input, self.ty.output), ) - return list(self.builder.add_op(clone_op, arg)) + return list(self.add_op(clone_op, arg)) case _: pass raise InternalGuppyError( diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index e5c5424ef..9f5f2962c 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -8,7 +8,6 @@ from guppylang_internals.ast_util import ( AstNode, - get_file, has_empty_body, with_loc, with_type, @@ -26,6 +25,7 @@ PyFunc, compile_call, load_with_args, + make_subprogram_record, parse_py_func, ) from guppylang_internals.definition.value import ( @@ -36,9 +36,9 @@ ) from guppylang_internals.diagnostic import Error from guppylang_internals.error import GuppyError -from guppylang_internals.metadata.debug_info import DISubprogram, HugrDebugInfo +from guppylang_internals.metadata.debug_info import HugrDebugInfo from guppylang_internals.nodes import GlobalCall -from guppylang_internals.span import SourceMap, to_span +from guppylang_internals.span import SourceMap from guppylang_internals.tys.param import Parameter from guppylang_internals.tys.subst import Inst, Subst from guppylang_internals.tys.ty import Type, UnitaryFlags @@ -133,11 +133,9 @@ def compile_outer( module: hf.Module = module node = module.declare_function(self.name, self.ty.to_hugr_poly(ctx)) - metadata = DISubprogram( - file=ctx.metadata_file_table.get_index(get_file(self.defined_at)), - line_no=to_span(self.defined_at).start.line, + node.metadata[HugrDebugInfo] = make_subprogram_record( + self.defined_at, ctx, is_decl=True ) - node.metadata[HugrDebugInfo] = metadata return CompiledFunctionDecl( self.id, self.name, diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 484623667..fd99e4904 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -47,9 +47,8 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import GuppyError -from guppylang_internals.metadata.common import GuppyMetadata, add_metadata +from guppylang_internals.metadata.common import FunctionMetadata, add_metadata from guppylang_internals.metadata.debug_info import ( - DILocation, DISubprogram, HugrDebugInfo, make_location_record, @@ -86,7 +85,7 @@ class RawFunctionDef(ParsableDef): unitary_flags: UnitaryFlags = field(default=UnitaryFlags.NoFlags, kw_only=True) - metadata: GuppyMetadata | None = field(default=None, kw_only=True) + metadata: FunctionMetadata | None = field(default=None, kw_only=True) def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": """Parses and checks the user-provided signature of the function.""" @@ -126,7 +125,7 @@ class ParsedFunctionDef(CheckableDef, CallableDef): description: str = field(default="function", init=False) - metadata: GuppyMetadata | None = field(default=None, kw_only=True) + metadata: FunctionMetadata | None = field(default=None, kw_only=True) def check(self, globals: Globals) -> "CheckedFunctionDef": """Type checks the body of the function.""" @@ -209,12 +208,8 @@ def monomorphize( func_def = module.module_root_builder().define_function( hugr_func_name, hugr_ty.body.input, hugr_ty.body.output, hugr_ty.params ) - metadata = DISubprogram( - file=ctx.metadata_file_table.get_index(get_file(self.defined_at)), - line_no=to_span(self.defined_at).start.line, - scope_line=to_span(self.defined_at.body[0]).start.line, - ) - self.metadata.set_debug_info(metadata) + assert self.metadata is not None + self.metadata.set_debug_info(make_subprogram_record(self.defined_at, ctx)) add_metadata( func_def, self.metadata, @@ -350,3 +345,26 @@ def parse_source(source_lines: list[str], line_offset: int) -> tuple[str, ast.AS else: node = ast.parse(source).body[0] return source, node, line_offset + + +# Note: Defined here as opposed to in `metadata.debug_info` to avoid circular imports +# due to using `CompilerContext` (not an issue for `make_location_record`). +def make_subprogram_record( + node: ast.FunctionDef, ctx: CompilerContext, is_decl: bool = False +) -> DISubprogram: + """Create a DISubprogram debug record for `node`, which should be a function + definition or declaration.""" + filename = get_file(node) + # If we can't fine a file for a node, we default to 0 which corresponds to the + # entrypoint file. + file_idx = ctx.metadata_file_table.get_index(filename) if filename else 0 + if is_decl or not node.body: + return DISubprogram( + file=file_idx, line_no=to_span(node).start.line, scope_line=None + ) + else: + return DISubprogram( + file=file_idx, + line_no=to_span(node).start.line, + scope_line=to_span(node.body[0]).start.line, + ) diff --git a/guppylang-internals/src/guppylang_internals/definition/struct.py b/guppylang-internals/src/guppylang_internals/definition/struct.py index f1b4a2402..3d6e4236b 100644 --- a/guppylang-internals/src/guppylang_internals/definition/struct.py +++ b/guppylang-internals/src/guppylang_internals/definition/struct.py @@ -269,7 +269,7 @@ class ConstructorCompiler(CustomCallCompiler): """Compiler for the `__new__` constructor method of a struct.""" def compile(self, args: list[Wire]) -> list[Wire]: - return list(self.builder.add(ops.MakeTuple()(*args))) + return list(self.add_op(ops.MakeTuple(), *args)) constructor_sig = FunctionType( inputs=[ diff --git a/guppylang-internals/src/guppylang_internals/metadata/common.py b/guppylang-internals/src/guppylang_internals/metadata/common.py index e7a398b05..c511019ef 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/common.py +++ b/guppylang-internals/src/guppylang_internals/metadata/common.py @@ -2,7 +2,7 @@ from typing import Any, ClassVar from hugr.hugr.node_port import ToNode -from hugr.metadata import JsonType, Metadata, NodeMetadata +from hugr.metadata import JsonType, Metadata from guppylang_internals.diagnostic import Fatal from guppylang_internals.error import GuppyError @@ -28,7 +28,7 @@ class ReservedMetadataKeysError(Fatal): @dataclass -class GuppyMetadata: +class FunctionMetadata: """Class for storing metadata to be attached to Hugr nodes during compilation.""" _node_metadata: dict[str, JsonType] = field(default_factory=dict) @@ -47,7 +47,9 @@ def set_max_qubits(self, max_qubits: int) -> None: self._node_metadata[MetadataMaxQubits.KEY] = max_qubits def get_debug_info(self) -> DebugRecord | None: - return self._node_metadata.get(HugrDebugInfo.KEY) + if HugrDebugInfo.KEY not in self._node_metadata: + return None + return DebugRecord.from_json(self._node_metadata.get(HugrDebugInfo.KEY)) def get_max_qubits(self) -> int | None: return self._node_metadata.get(MetadataMaxQubits.KEY) @@ -59,7 +61,7 @@ def reserved_keys(cls) -> set[str]: def add_metadata( node: ToNode, - metadata: GuppyMetadata | None = None, + metadata: FunctionMetadata | None = None, *, additional_metadata: dict[str, Any] | None = None, ) -> None: @@ -73,7 +75,7 @@ def add_metadata( node.metadata[key] = metadata_dict[key] if additional_metadata is not None: - reserved_keys = GuppyMetadata.reserved_keys() + reserved_keys = FunctionMetadata.reserved_keys() used_reserved_keys = reserved_keys.intersection(additional_metadata.keys()) if len(used_reserved_keys) > 0: raise GuppyError(ReservedMetadataKeysError(None, keys=used_reserved_keys)) diff --git a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py index baefa129a..cd6573d20 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py +++ b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py @@ -1,9 +1,11 @@ +import ast from abc import ABC, abstractmethod from dataclasses import dataclass -from guppylang_internals.span import ToSpan, to_span from hugr.metadata import JsonType, Metadata +from guppylang_internals.span import to_span + @dataclass class DebugRecord(ABC): @@ -139,7 +141,7 @@ def from_json(cls, value: JsonType) -> "DILocation": return DILocation(column=int(value["column"]), line_no=int(value["line_no"])) -def make_location_record(node: ToSpan) -> DILocation: +def make_location_record(node: ast.AST) -> DILocation: """Creates a DILocation metadata record for `node`.""" return DILocation( line_no=to_span(node).start.line, column=to_span(node).start.column diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py index 7eb655239..69e91c2bc 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py @@ -292,7 +292,7 @@ def build_classical_array(self, elems: list[Wire]) -> Wire: def build_linear_array(self, elems: list[Wire]) -> Wire: """Lowers a call to `array.__new__` for linear arrays.""" - return self.builder.add_op(array_new(self.elem_ty, len(elems)), *elems) + return self.add_op(array_new(self.elem_ty, len(elems)), *elems) def compile(self, args: list[Wire]) -> list[Wire]: if self.elem_ty.type_bound() == ht.TypeBound.Linear: @@ -306,7 +306,7 @@ class ArrayGetitemCompiler(ArrayCompiler): def _build_classical_getitem(self, array: Wire, idx: Wire) -> CallReturnWires: """Constructs `__getitem__` for classical arrays.""" - idx = self.builder.add_op(convert_itousize(), idx) + idx = self.add_op(convert_itousize(), idx) opt_elem, arr = self.builder.add_op( array_get(self.elem_ty, self.length), @@ -321,8 +321,8 @@ def _build_classical_getitem(self, array: Wire, idx: Wire) -> CallReturnWires: def _build_linear_getitem(self, array: Wire, idx: Wire) -> CallReturnWires: """Constructs `array.__getitem__` for linear arrays.""" - idx = self.builder.add_op(convert_itousize(), idx) - arr, elem = self.builder.add_op( + idx = self.add_op(convert_itousize(), idx) + arr, elem = self.add_op( barray_borrow(self.elem_ty, self.length), array, idx, @@ -360,8 +360,8 @@ def _build_classical_setitem( self, array: Wire, idx: Wire, elem: Wire ) -> CallReturnWires: """Constructs `__setitem__` for classical arrays.""" - idx = self.builder.add_op(convert_itousize(), idx) - result = self.builder.add_op( + idx = self.add_op(convert_itousize(), idx) + result = self.add_op( array_set(self.elem_ty, self.length), array, idx, @@ -378,8 +378,8 @@ def _build_linear_setitem( self, array: Wire, idx: Wire, elem: Wire ) -> CallReturnWires: """Constructs `array.__setitem__` for linear arrays.""" - idx = self.builder.add_op(convert_itousize(), idx) - arr = self.builder.add_op( + idx = self.add_op(convert_itousize(), idx) + arr = self.add_op( barray_return(self.elem_ty, self.length), array, idx, @@ -410,7 +410,7 @@ class ArrayDiscardAllUsedCompiler(ArrayCompiler): def compile(self, args: list[Wire]) -> list[Wire]: if self.elem_ty.type_bound() == ht.TypeBound.Linear: [arr] = args - self.builder.add_op( + self.add_op( barray_discard_all_borrowed(self.elem_ty, self.length), arr, ) @@ -422,11 +422,11 @@ class ArrayIsBorrowedCompiler(ArrayCompiler): def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: [array, idx] = args - idx = self.builder.add_op(convert_itousize(), idx) - array, b = self.builder.add_op( + idx = self.add_op(convert_itousize(), idx) + array, b = self.add_op( barray_is_borrowed(self.elem_ty, self.length), array, idx ) - b = self.builder.add_op(make_opaque(), b) + b = self.add_op(make_opaque(), b) return CallReturnWires(regular_returns=[b], inout_returns=[array]) def compile(self, args: list[Wire]) -> list[Wire]: @@ -439,12 +439,12 @@ class ArraySwapCompiler(ArrayCompiler): def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: [array, idx1, idx2] = args - idx1 = self.builder.add_op(convert_itousize(), idx1) - idx2 = self.builder.add_op(convert_itousize(), idx2) + idx1 = self.add_op(convert_itousize(), idx1) + idx2 = self.add_op(convert_itousize(), idx2) # Swap returns Either(left=array, right=array) # Left (case 0) is failure, right (case 1) is success - either_result = self.builder.add_op( + either_result = self.add_op( array_swap(self.elem_ty, self.length), array, idx1, diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/either.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/either.py index 8f460eb62..f313f5bd3 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/either.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/either.py @@ -77,8 +77,8 @@ def compile(self, args: list[Wire]) -> list[Wire]: assert isinstance(inp_arg, TypeArg) [inp] = args # Unpack the single input into a row - inp_row = unpack_wire(inp, inp_arg.ty, self.builder, self.ctx) - return [self.builder.add_op(ops.Tag(self.tag, ty), *inp_row)] + inp_row = unpack_wire(inp, inp_arg.ty, self.builder, self.ctx, self.node) + return [self.add_op(ops.Tag(self.tag, ty), *inp_row)] class EitherTestCompiler(EitherCompiler): @@ -113,7 +113,8 @@ def compile(self, args: list[Wire]) -> list[Wire]: with cond.add_case(i) as case: if i == self.tag: out = case.add_op( - ops.Tag(1, ht.Option(*target_tys)), *case.inputs() + ops.Tag(1, ht.Option(*target_tys)), + *case.inputs(), ) else: out = case.add_op(ops.Tag(0, ht.Option(*target_tys))) @@ -140,4 +141,4 @@ def compile(self, args: list[Wire]) -> list[Wire]: # Pack outputs into a single wire. We're not allowed to return a row since the # signature has a generic return type (also see `TupleType.preserve`) return_ty = get_type(self.node) - return [pack_returns(list(out), return_ty, self.builder, self.ctx)] + return [pack_returns(list(out), return_ty, self.builder, self.ctx, self.node)] diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py index 389d6faad..7c11557c2 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py @@ -112,8 +112,8 @@ def build_classical_getitem( elem_ty: ht.Type, ) -> CallReturnWires: """Lowers a call to `list.__getitem__` for classical lists.""" - idx = self.builder.add_op(convert_itousize(), idx) - result = self.builder.add_op(list_get(elem_ty), list_wire, idx) + idx = self.add_op(convert_itousize(), idx) + result = self.add_op(list_get(elem_ty), list_wire, idx) elem = build_unwrap(self.builder, result, "List index out of bounds") return CallReturnWires(regular_returns=[elem], inout_returns=[list_wire]) @@ -128,9 +128,9 @@ def build_linear_getitem( # implementation of the list type ensures that linear element types are turned # into optionals. elem_opt_ty = ht.Option(elem_ty) - none = self.builder.add_op(ops.Tag(0, elem_opt_ty)) - idx = self.builder.add_op(convert_itousize(), idx) - list_wire, result = self.builder.add_op( + none = self.add_op(ops.Tag(0, elem_opt_ty)) + idx = self.add_op(convert_itousize(), idx) + list_wire, result = self.add_op( list_set(elem_opt_ty), list_wire, idx, none ) elem_opt = build_unwrap_right(self.builder, result, "List index out of bounds") @@ -167,8 +167,8 @@ def build_classical_setitem( elem_ty: ht.Type, ) -> CallReturnWires: """Lowers a call to `list.__setitem__` for classical lists.""" - idx = self.builder.add_op(convert_itousize(), idx) - list_wire, result = self.builder.add_op(list_set(elem_ty), list_wire, idx, elem) + idx = self.add_op(convert_itousize(), idx) + list_wire, result = self.add_op(list_set(elem_ty), list_wire, idx, elem) # Unwrap the result, but we don't have to hold onto the returned old value build_unwrap_right(self.builder, result, "List index out of bounds") return CallReturnWires(regular_returns=[], inout_returns=[list_wire]) @@ -183,9 +183,9 @@ def build_linear_setitem( """Lowers a call to `array.__setitem__` for linear arrays.""" # Embed the element into an optional elem_opt_ty = ht.Option(elem_ty) - elem = self.builder.add_op(ops.Some(elem_ty), elem) - idx = self.builder.add_op(convert_itousize(), idx) - list_wire, result = self.builder.add_op( + elem = self.add_op(ops.Some(elem_ty), elem) + idx = self.add_op(convert_itousize(), idx) + list_wire, result = self.add_op( list_set(elem_opt_ty), list_wire, idx, elem ) old_elem_opt = build_unwrap_right( @@ -223,7 +223,7 @@ def build_classical_pop( elem_ty: ht.Type, ) -> CallReturnWires: """Lowers a call to `list.pop` for classical lists.""" - list_wire, result = self.builder.add_op(list_pop(elem_ty), list_wire) + list_wire, result = self.add_op(list_pop(elem_ty), list_wire) elem = build_unwrap(self.builder, result, "List index out of bounds") return CallReturnWires(regular_returns=[elem], inout_returns=[list_wire]) @@ -234,7 +234,7 @@ def build_linear_pop( ) -> CallReturnWires: """Lowers a call to `list.pop` for linear lists.""" elem_opt_ty = ht.Option(elem_ty) - list_wire, result = self.builder.add_op(list_pop(elem_opt_ty), list_wire) + list_wire, result = self.add_op(list_pop(elem_opt_ty), list_wire) elem_opt = build_unwrap(self.builder, result, "List index out of bounds") elem = build_unwrap( self.builder, elem_opt, "Linear list element has already been used" @@ -264,7 +264,7 @@ def build_classical_push( elem_ty: ht.Type, ) -> CallReturnWires: """Lowers a call to `list.push` for classical lists.""" - list_wire = self.builder.add_op(list_push(elem_ty), list_wire, elem) + list_wire = self.add_op(list_push(elem_ty), list_wire, elem) return CallReturnWires(regular_returns=[], inout_returns=[list_wire]) def build_linear_push( @@ -276,8 +276,8 @@ def build_linear_push( """Lowers a call to `list.push` for linear lists.""" # Wrap element into an optional elem_opt_ty = ht.Option(elem_ty) - elem_opt = self.builder.add_op(ops.Some(elem_ty), elem) - list_wire = self.builder.add_op(list_push(elem_opt_ty), list_wire, elem_opt) + elem_opt = self.add_op(ops.Some(elem_ty), elem) + list_wire = self.add_op(list_push(elem_opt_ty), list_wire, elem_opt) return CallReturnWires(regular_returns=[], inout_returns=[list_wire]) def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: @@ -307,8 +307,8 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: elem_ty = elem_ty_arg.ty.to_hugr(self.ctx) if elem_ty_arg.ty.linear: elem_ty = ht.Option(elem_ty) - list_wire, length = self.builder.add_op(list_length(elem_ty), list_wire) - length = self.builder.add_op(convert_ifromusize(), length) + list_wire, length = self.add_op(list_length(elem_ty), list_wire) + length = self.add_op(convert_ifromusize(), length) return CallReturnWires(regular_returns=[length], inout_returns=[list_wire]) def compile(self, args: list[Wire]) -> list[Wire]: diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/option.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/option.py index 988a3d8c8..d5a6bf48c 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/option.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/option.py @@ -39,7 +39,7 @@ def __init__(self, tag: int): self.tag = tag def compile(self, args: list[Wire]) -> list[Wire]: - return [self.builder.add_op(ops.Tag(self.tag, self.option_ty), *args)] + return [self.add_op(ops.Tag(self.tag, self.option_ty), *args)] class OptionTestCompiler(OptionCompiler): diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py index 46703cfe2..4dd8f0978 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py @@ -66,11 +66,11 @@ def compile(self, args: list[Wire]) -> list[Wire]: args.append(tys.BoundedNatArg(NumericType.INT_WIDTH)) # Bool results need an extra conversion into regular hugr bools if is_bool_type(ty): - value = self.builder.add_op(read_bool(), value) + value = self.add_op(read_bool(), value) hugr_ty = tys.Bool op = RESULT_EXTENSION.get_op(self.op_name) sig = tys.FunctionType(input=[hugr_ty], output=[]) - self.builder.add_op(op.instantiate(args, sig), value) + self.add_op(op.instantiate(args, sig), value) return [] @@ -97,17 +97,17 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: # argument). hugr_elem_ty = elem_ty.to_hugr(self.ctx) hugr_size = size_arg.to_hugr(self.ctx) - arr, out_arr = self.builder.add_op(array_clone(hugr_elem_ty, hugr_size), arr) + arr, out_arr = self.add_op(array_clone(hugr_elem_ty, hugr_size), arr) # For bool arrays, we furthermore need to coerce a read on all the array # elements if is_bool_type(elem_ty): array_read = array_read_bool(self.ctx) array_read = self.builder.load_function(array_read) map_op = array_map(OpaqueBool, hugr_size, tys.Bool) - arr = self.builder.add_op(map_op, arr, array_read).out(0) + arr = self.add_op(map_op, arr, array_read).out(0) hugr_elem_ty = tys.Bool # Turn `borrow_array` into regular `array` - arr = self.builder.add_op(array_to_std_array(hugr_elem_ty, hugr_size), arr).out( + arr = self.add_op(array_to_std_array(hugr_elem_ty, hugr_size), arr).out( 0 ) @@ -117,7 +117,7 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: if self.with_int_width: args.append(tys.BoundedNatArg(NumericType.INT_WIDTH)) op = ops.ExtOp(RESULT_EXTENSION.get_op(self.op_name), signature=sig, args=args) - self.builder.add_op(op, arr) + self.add_op(op, arr) return CallReturnWires([], [out_arr]) diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/prelude.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/prelude.py index 3b043ffda..f96bc4aa3 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/prelude.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/prelude.py @@ -254,7 +254,7 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: output=[ht.Either([error_type()], self.ty.output)], ) op = self.op(opt_func_type, self.type_args, self.ctx) - either = self.builder.add_op(op, *args) + either = self.add_op(op, *args) result = unwrap_result(self.builder, self.ctx, either) return CallReturnWires(regular_returns=[result], inout_returns=[]) @@ -269,7 +269,7 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: [ht.ListArg([ht.TypeTypeArg(ty) for ty in tys])] ) - barrier_n = self.builder.add_op(op, *args) + barrier_n = self.add_op(op, *args) return CallReturnWires( regular_returns=[], inout_returns=[barrier_n[i] for i in range(len(tys))] diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 5140fae94..bca11789b 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -26,7 +26,6 @@ from guppylang_internals.definition.function import ( RawFunctionDef, ) -from guppylang_internals.metadata.common import GuppyMetadata from guppylang_internals.definition.overloaded import OverloadedFunctionDef from guppylang_internals.definition.parameter import ( ConstVarDef, @@ -42,6 +41,7 @@ from guppylang_internals.definition.ty import TypeDef from guppylang_internals.dummy_decorator import _DummyGuppy, sphinx_running from guppylang_internals.engine import DEF_STORE +from guppylang_internals.metadata.common import FunctionMetadata from guppylang_internals.span import Loc, SourceMap, Span from guppylang_internals.tracing.util import hide_trace from guppylang_internals.tys.arg import Argument @@ -646,7 +646,7 @@ def _with_optional_kwargs( @hide_trace -def _parse_kwargs(kwargs: GuppyKwargs) -> tuple[UnitaryFlags, GuppyMetadata]: +def _parse_kwargs(kwargs: GuppyKwargs) -> tuple[UnitaryFlags, FunctionMetadata]: """Parses the kwargs dict specified in the `@guppy` decorator into `UnitaryFlags` and other metadata that will be passed onto the compiled function as is. """ @@ -660,8 +660,9 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> tuple[UnitaryFlags, GuppyMetadata]: if kwargs.pop("power", False): flags |= UnitaryFlags.Power - metadata = GuppyMetadata() - metadata.set_max_qubits(kwargs.pop("max_qubits", None)) + metadata = FunctionMetadata() + if "max_qubits" in kwargs: + metadata.set_max_qubits(kwargs.pop("max_qubits")) if remaining := next(iter(kwargs), None): err = f"Unknown keyword argument: `{remaining}`" diff --git a/tests/metadata/test_debug_info.py b/tests/metadata/test_debug_info.py index 454892ab1..562153c6b 100644 --- a/tests/metadata/test_debug_info.py +++ b/tests/metadata/test_debug_info.py @@ -1,12 +1,14 @@ +from guppylang.std.debug import state_result from guppylang_internals.metadata.debug_info import ( DICompileUnit, DILocation, DISubprogram, HugrDebugInfo, ) -from hugr.ops import Call, FuncDecl, FuncDefn +from hugr.ops import Call, ExtOp, FuncDecl, FuncDefn from guppylang import guppy +from guppylang.std.quantum import discard, qubit from tests.resources.metadata_example import bar, baz @@ -51,8 +53,8 @@ def foo() -> None: assert HugrDebugInfo in func.metadata debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) assert debug_info.file == 0 - assert debug_info.line_no == 33 - assert debug_info.scope_line == 34 + assert debug_info.line_no == 35 + assert debug_info.scope_line == 36 case "bar": assert HugrDebugInfo in func.metadata debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) @@ -79,5 +81,19 @@ def foo() -> None: if isinstance(node_data.op, Call): assert HugrDebugInfo in node.metadata debug_info = DILocation.from_json(node.metadata[HugrDebugInfo.KEY]) - assert debug_info.line_no == 75 + assert debug_info.line_no == 77 assert debug_info.column == 8 + + +def test_custom_function(): + @guppy + def foo() -> None: + q = qubit() + state_result("tag", q) + discard(q) + + hugr = foo.compile().modules[0] + for node, node_data in hugr.nodes(): + if isinstance(node_data.op, ExtOp) and "unpack" not in node_data.op.name(): + assert HugrDebugInfo in node.metadata + diff --git a/tests/metadata/test_metadata.py b/tests/metadata/test_metadata.py index ee734e6f0..36438251d 100644 --- a/tests/metadata/test_metadata.py +++ b/tests/metadata/test_metadata.py @@ -5,7 +5,7 @@ import pytest from guppylang_internals.error import GuppyError from guppylang_internals.metadata.common import ( - GuppyMetadata, + FunctionMetadata, MetadataAlreadySetError, ReservedMetadataKeysError, add_metadata, @@ -17,7 +17,7 @@ def test_add_metadata(): mock_hugr_node = Mock() mock_hugr_node.metadata = NodeMetadata({"some-key": "some-value"}) - guppy_metadata = GuppyMetadata() + guppy_metadata = FunctionMetadata() guppy_metadata.set_max_qubits(5) add_metadata(mock_hugr_node, guppy_metadata) @@ -55,13 +55,14 @@ def test_add_metadata_no_reserved_metadata(): def test_add_metadata_metadata_already_set(): mock_hugr_node = Mock() - mock_hugr_node.metadata = NodeMetadata({ - "tket.hint.max_qubits": 1, - "preset-key": "preset-value", - }) - - - guppy_metadata = GuppyMetadata() + mock_hugr_node.metadata = NodeMetadata( + { + "tket.hint.max_qubits": 1, + "preset-key": "preset-value", + } + ) + + guppy_metadata = FunctionMetadata() guppy_metadata.set_max_qubits(5) with pytest.raises( GuppyError, @@ -85,7 +86,7 @@ def test_add_metadata_property_max_qubits(): mock_hugr_node = Mock() mock_hugr_node.metadata = NodeMetadata({}) - guppy_metadata = GuppyMetadata() + guppy_metadata = FunctionMetadata() guppy_metadata.set_max_qubits(5) add_metadata(mock_hugr_node, guppy_metadata) From d0886303a34add079f284ac3db8ed2d156fadd4f Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Thu, 5 Mar 2026 15:48:56 +0000 Subject: [PATCH 10/17] Add global flag for making debug metadata optional --- .../compiler/expr_compiler.py | 3 ++- .../src/guppylang_internals/debug_mode.py | 18 +++++++++++++++ .../definition/declaration.py | 8 ++++--- .../definition/function.py | 9 +++++--- .../guppylang_internals/definition/traced.py | 17 +++++++++++--- .../src/guppylang_internals/engine.py | 16 ++++++++------ tests/metadata/test_debug_info.py | 22 ++++++++++++++++--- 7 files changed, 73 insertions(+), 20 deletions(-) create mode 100644 guppylang-internals/src/guppylang_internals/debug_mode.py diff --git a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py index d9357120c..77a4c8bb7 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py +++ b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py @@ -28,6 +28,7 @@ GlobalConstId, ) from guppylang_internals.compiler.hugr_extension import PartialOp +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.custom import CustomFunctionDef from guppylang_internals.definition.value import ( CallableDef, @@ -773,7 +774,7 @@ def add_op( ) -> Node: """Adds an op to the builder, with optional debug info.""" op_node = builder.add_op(op, *args) - if ast_node is not None and get_file(ast_node) is not None: + if debug_mode_enabled() and ast_node is not None and get_file(ast_node) is not None: op_node.metadata[HugrDebugInfo] = make_location_record(ast_node) return op_node diff --git a/guppylang-internals/src/guppylang_internals/debug_mode.py b/guppylang-internals/src/guppylang_internals/debug_mode.py new file mode 100644 index 000000000..6b35fd8de --- /dev/null +++ b/guppylang-internals/src/guppylang_internals/debug_mode.py @@ -0,0 +1,18 @@ +"""Global state for determining whether to attach debug information to Hugr nodes +during compilation.""" + +DEBUG_MODE_ENABLED = False + + +def turn_on_debug_mode() -> None: + global DEBUG_MODE_ENABLED + DEBUG_MODE_ENABLED = True + + +def turn_off_debug_mode() -> None: + global DEBUG_MODE_ENABLED + DEBUG_MODE_ENABLED = False + + +def debug_mode_enabled() -> bool: + return DEBUG_MODE_ENABLED diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index 9f5f2962c..db8558b35 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -20,6 +20,7 @@ DFContainer, require_monomorphization, ) +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import CompilableDef, ParsableDef from guppylang_internals.definition.function import ( PyFunc, @@ -133,9 +134,10 @@ def compile_outer( module: hf.Module = module node = module.declare_function(self.name, self.ty.to_hugr_poly(ctx)) - node.metadata[HugrDebugInfo] = make_subprogram_record( - self.defined_at, ctx, is_decl=True - ) + if debug_mode_enabled(): + node.metadata[HugrDebugInfo] = make_subprogram_record( + self.defined_at, ctx, is_decl=True + ) return CompiledFunctionDecl( self.id, self.name, diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index fd99e4904..d008326eb 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -32,6 +32,7 @@ PartiallyMonomorphizedArgs, ) from guppylang_internals.compiler.func_compiler import compile_global_func_def +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import ( CheckableDef, MonomorphizableDef, @@ -208,8 +209,9 @@ def monomorphize( func_def = module.module_root_builder().define_function( hugr_func_name, hugr_ty.body.input, hugr_ty.body.output, hugr_ty.params ) - assert self.metadata is not None - self.metadata.set_debug_info(make_subprogram_record(self.defined_at, ctx)) + if debug_mode_enabled(): + assert self.metadata is not None + self.metadata.set_debug_info(make_subprogram_record(self.defined_at, ctx)) add_metadata( func_def, self.metadata, @@ -304,7 +306,8 @@ def compile_call( type_args = [arg.to_hugr(dfg.ctx) for arg in type_args] num_returns = len(type_to_row(ty.output)) call = dfg.builder.call(func, *args, instantiation=func_ty, type_args=type_args) - call.metadata[HugrDebugInfo] = make_location_record(call_ast) + if debug_mode_enabled(): + call.metadata[HugrDebugInfo] = make_location_record(call_ast) return CallReturnWires( regular_returns=list(call[:num_returns]), inout_returns=list(call[num_returns:]), diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index 5170af7fb..1728c0d7c 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -19,11 +19,15 @@ check_signature, ) from guppylang_internals.compiler.core import CompilerContext, DFContainer +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import ( CompilableDef, ParsableDef, ) -from guppylang_internals.definition.function import parse_py_func +from guppylang_internals.definition.function import ( + make_subprogram_record, + parse_py_func, +) from guppylang_internals.definition.value import ( CallableDef, CallReturnWires, @@ -31,7 +35,11 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import GuppyError -from guppylang_internals.metadata.debug_info import DILocation, HugrDebugInfo, make_location_record +from guppylang_internals.metadata.debug_info import ( + DILocation, + HugrDebugInfo, + make_location_record, +) from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap, to_span from guppylang_internals.tys.subst import Inst, Subst @@ -92,6 +100,8 @@ def compile_outer( func_def = module.module_root_builder().define_function( self.name, func_type.body.input, func_type.body.output, func_type.params ) + if debug_mode_enabled(): + func_def.metadata[HugrDebugInfo] = make_subprogram_record(self.defined_at) return CompiledTracedFunctionDef( self.id, self.name, @@ -140,7 +150,8 @@ def compile_call( call = dfg.builder.call( self.func_def, *args, instantiation=func_ty, type_args=type_args ) - call.metadata[HugrDebugInfo] = make_location_record(node) + if debug_mode_enabled(): + call.metadata[HugrDebugInfo] = make_location_record(node) return CallReturnWires( regular_returns=list(call[:num_returns]), inout_returns=list(call[num_returns:]), diff --git a/guppylang-internals/src/guppylang_internals/engine.py b/guppylang-internals/src/guppylang_internals/engine.py index c5b807a2d..20c14d459 100644 --- a/guppylang-internals/src/guppylang_internals/engine.py +++ b/guppylang-internals/src/guppylang_internals/engine.py @@ -13,6 +13,7 @@ from semver import Version import guppylang_internals +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import ( CheckableDef, CheckedDef, @@ -311,13 +312,14 @@ def compile(self, id: DefId) -> ModulePointer: graph.hugr.entrypoint = compiled_def.hugr_node # Add debug info about the module to the root node - module_info = DICompileUnit( - directory=Path.cwd().as_uri(), - # We know this file is always the first entry in the file table. - filename=ctx.metadata_file_table.get_index(filename), - file_table=ctx.metadata_file_table.table, - ) - graph.hugr.module_root.metadata[HugrDebugInfo] = module_info + if debug_mode_enabled(): + module_info = DICompileUnit( + directory=Path.cwd().as_uri(), + # We know this file is always the first entry in the file table. + filename=ctx.metadata_file_table.get_index(filename), + file_table=ctx.metadata_file_table.table, + ) + graph.hugr.module_root.metadata[HugrDebugInfo] = module_info # Use cached base extensions and registry, only add additional extensions base_extensions = self._get_base_packaged_extensions() diff --git a/tests/metadata/test_debug_info.py b/tests/metadata/test_debug_info.py index 562153c6b..ba30770ed 100644 --- a/tests/metadata/test_debug_info.py +++ b/tests/metadata/test_debug_info.py @@ -1,4 +1,5 @@ from guppylang.std.debug import state_result +from guppylang_internals.debug_mode import turn_off_debug_mode, turn_on_debug_mode from guppylang_internals.metadata.debug_info import ( DICompileUnit, DILocation, @@ -11,6 +12,8 @@ from guppylang.std.quantum import discard, qubit from tests.resources.metadata_example import bar, baz +turn_on_debug_mode() + def get_last_uri_part(uri: str) -> str: return uri.split("/")[-1] @@ -53,8 +56,8 @@ def foo() -> None: assert HugrDebugInfo in func.metadata debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) assert debug_info.file == 0 - assert debug_info.line_no == 35 - assert debug_info.scope_line == 36 + assert debug_info.line_no == 37 + assert debug_info.scope_line == 38 case "bar": assert HugrDebugInfo in func.metadata debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) @@ -81,7 +84,7 @@ def foo() -> None: if isinstance(node_data.op, Call): assert HugrDebugInfo in node.metadata debug_info = DILocation.from_json(node.metadata[HugrDebugInfo.KEY]) - assert debug_info.line_no == 77 + assert debug_info.line_no == 79 assert debug_info.column == 8 @@ -97,3 +100,16 @@ def foo() -> None: if isinstance(node_data.op, ExtOp) and "unpack" not in node_data.op.name(): assert HugrDebugInfo in node.metadata + +def test_turn_off_debug_mode(): + turn_off_debug_mode() + + @guppy + def foo() -> None: + q = qubit() + state_result("tag", q) + discard(q) + + hugr = foo.compile().modules[0] + for node, _ in hugr.nodes(): + assert HugrDebugInfo not in node.metadata From e034a9bd177657d5db29e4da9823040c14ad5ea2 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Mon, 9 Mar 2026 13:18:00 +0000 Subject: [PATCH 11/17] Extend tests + various special case fixes --- .../compiler/expr_compiler.py | 10 +- .../definition/pytket_circuits.py | 49 +++++++- .../guppylang_internals/definition/traced.py | 7 +- .../std/_internal/compiler/array.py | 13 ++- tests/metadata/test_debug_info.py | 105 ++++++++++++++---- tests/resources/metadata_example.py | 18 +++ 6 files changed, 174 insertions(+), 28 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py index 77a4c8bb7..ad294b6c1 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py +++ b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py @@ -633,14 +633,20 @@ def visit_StateResultExpr(self, node: StateResultExpr) -> Wire: qubit_arr_out, ast_node=node, ) - qubits_out = unpack_array(self.builder, qubit_arr_out) + qubits_out = unpack_array(self.builder, qubit_arr_out, ast_node=node) else: # If the input is an array of qubits, we need to convert to a standard # array. qubits_in = [self.visit(node.args[1])] qubits_out = [ apply_array_op_with_conversions( - self.ctx, self.builder, op, ht.Qubit, num_qubits_arg, qubits_in[0] + self.ctx, + self.builder, + op, + ht.Qubit, + num_qubits_arg, + qubits_in[0], + ast_node=node, ) ] diff --git a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py index 9d5a81b9c..af670d44f 100644 --- a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py +++ b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py @@ -19,6 +19,7 @@ check_signature, ) from guppylang_internals.compiler.core import CompilerContext, DFContainer +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import ( CompilableDef, ParsableDef, @@ -28,6 +29,7 @@ PyFunc, compile_call, load_with_args, + make_subprogram_record, parse_py_func, ) from guppylang_internals.definition.ty import TypeDef @@ -39,6 +41,12 @@ ) from guppylang_internals.engine import ENGINE from guppylang_internals.error import GuppyError, InternalGuppyError +from guppylang_internals.metadata.debug_info import ( + DILocation, + DISubprogram, + HugrDebugInfo, + make_location_record, +) from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap, Span, ToSpan from guppylang_internals.std._internal.compiler.array import ( @@ -98,7 +106,13 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedPytketDef": ) raise GuppyError(err) return ParsedPytketDef( - self.id, self.name, func_ast, stub_signature, self.input_circuit, False + self.id, + self.name, + func_ast, + stub_signature, + self.input_circuit, + False, + None, ) @@ -134,6 +148,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedPytketDef": circuit_signature, self.input_circuit, self.use_arrays, + self.source_span, ) @@ -154,6 +169,8 @@ class ParsedPytketDef(CallableDef, CompilableDef): input_circuit: Any use_arrays: bool + source_span: Span | None # Only set for load_pytket for debug purposes. + description: str = field(default="pytket circuit", init=False) def compile_outer( @@ -180,6 +197,24 @@ def compile_outer( outer_func = module.module_root_builder().define_function( self.name, func_type.body.input, func_type.body.output ) + # Mark both inner and outer function with the same location metadata. + if debug_mode_enabled(): + # Function stub case. + if self.defined_at is not None: + func_metadata = make_subprogram_record( + self.defined_at, ctx, is_decl=True + ) + # Load pytket case, + elif self.source_span is not None: + file_idx = ctx.metadata_file_table.get_index( + self.source_span.file + ) + func_metadata = DISubprogram( + file=file_idx, + line_no=self.source_span.start.line, + scope_line=None, + ) + outer_func.metadata[HugrDebugInfo] = func_metadata # Number of qubit inputs in the outer function. offset = ( @@ -249,6 +284,16 @@ def compile_outer( call_node = outer_func.call( hugr_func, *(input_list + bool_wires + param_wires) ) + if debug_mode_enabled(): + if self.defined_at is not None: + call_node.metadata[HugrDebugInfo] = make_location_record( + self.defined_at + ) + elif self.source_span is not None: + call_node.metadata[HugrDebugInfo] = DILocation( + column=self.source_span.start.column, + line_no=self.source_span.start.line, + ) # Pytket circuit hugr has qubit and bool wires in the opposite # order to Guppy output wires. @@ -300,6 +345,7 @@ def compile_outer( self.ty, self.input_circuit, self.use_arrays, + self.source_span, outer_func, ) @@ -375,6 +421,7 @@ def _signature_from_circuit( """Helper function for inferring a function signature from a pytket circuit.""" # May want to set proper unitary flags in the future. from guppylang.std.angles import angle # Avoid circular imports + from guppylang.std.quantum import qubit assert isinstance(qubit, GuppyDefinition) diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index 1728c0d7c..39e93fc5b 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -36,12 +36,11 @@ ) from guppylang_internals.error import GuppyError from guppylang_internals.metadata.debug_info import ( - DILocation, HugrDebugInfo, make_location_record, ) from guppylang_internals.nodes import GlobalCall -from guppylang_internals.span import SourceMap, to_span +from guppylang_internals.span import SourceMap from guppylang_internals.tys.subst import Inst, Subst from guppylang_internals.tys.ty import FunctionType, Type, type_to_row @@ -101,7 +100,9 @@ def compile_outer( self.name, func_type.body.input, func_type.body.output, func_type.params ) if debug_mode_enabled(): - func_def.metadata[HugrDebugInfo] = make_subprogram_record(self.defined_at) + func_def.metadata[HugrDebugInfo] = make_subprogram_record( + self.defined_at, ctx + ) return CompiledTracedFunctionDef( self.id, self.name, diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py index 69e91c2bc..4863938d4 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py @@ -9,6 +9,7 @@ from hugr import tys as ht from hugr.std.collections.borrow_array import EXTENSION +from guppylang_internals.ast_util import AstNode from guppylang_internals.definition.custom import CustomCallCompiler from guppylang_internals.definition.value import CallReturnWires from guppylang_internals.error import InternalGuppyError @@ -248,13 +249,19 @@ def array_swap(elem_ty: ht.Type, length: ht.TypeArg) -> ops.ExtOp: P = TypeVar("P", bound=ops.DfParentOp) -def unpack_array(builder: DfBase[P], array: Wire) -> list[Wire]: +def unpack_array( + builder: DfBase[P], array: Wire, ast_node: AstNode | None = None +) -> list[Wire]: """Unpacks a fixed length array into its elements.""" + from guppylang_internals.compiler.expr_compiler import add_op + array_ty = builder.hugr.port_type(array.out_port()) assert isinstance(array_ty, ht.ExtType) match array_ty.args: case [ht.BoundedNatArg(length), ht.TypeTypeArg(elem_ty)]: - res = builder.add_op(array_unpack(elem_ty, length), array) + res = add_op( + builder, array_unpack(elem_ty, length), array, ast_node=ast_node + ) return [res[i] for i in range(length)] case _: raise InternalGuppyError("Invalid array type args") @@ -308,7 +315,7 @@ def _build_classical_getitem(self, array: Wire, idx: Wire) -> CallReturnWires: """Constructs `__getitem__` for classical arrays.""" idx = self.add_op(convert_itousize(), idx) - opt_elem, arr = self.builder.add_op( + opt_elem, arr = self.add_op( array_get(self.elem_ty, self.length), array, idx, diff --git a/tests/metadata/test_debug_info.py b/tests/metadata/test_debug_info.py index ba30770ed..1e32fc97e 100644 --- a/tests/metadata/test_debug_info.py +++ b/tests/metadata/test_debug_info.py @@ -6,11 +6,18 @@ DISubprogram, HugrDebugInfo, ) -from hugr.ops import Call, ExtOp, FuncDecl, FuncDefn +from hugr.ops import Call, ExtOp, FuncDecl, FuncDefn, MakeTuple from guppylang import guppy -from guppylang.std.quantum import discard, qubit -from tests.resources.metadata_example import bar, baz +from guppylang.std import array +from guppylang.std.quantum import discard, discard_array, qubit +from tests.resources.metadata_example import ( + bar, + baz, + comptime_bar, + pytket_bar_load, + pytket_bar_stub, +) turn_on_debug_mode() @@ -38,6 +45,11 @@ def test_subprogram(): def foo() -> None: bar() baz() + comptime_bar() + q = qubit() + pytket_bar_load(q) + pytket_bar_stub(q) + discard(q) hugr = foo.compile().modules[0] meta = hugr.module_root.metadata @@ -56,20 +68,41 @@ def foo() -> None: assert HugrDebugInfo in func.metadata debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) assert debug_info.file == 0 - assert debug_info.line_no == 37 - assert debug_info.scope_line == 38 + assert debug_info.line_no == 45 + assert debug_info.scope_line == 46 case "bar": assert HugrDebugInfo in func.metadata debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) assert debug_info.file == 1 - assert debug_info.line_no == 7 - assert debug_info.scope_line == 10 + assert debug_info.line_no == 10 + assert debug_info.scope_line == 13 case "baz": assert HugrDebugInfo in func.metadata debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) assert debug_info.file == 1 - assert debug_info.line_no == 14 + assert debug_info.line_no == 17 assert debug_info.scope_line is None + case "comptime_bar": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 21 + assert debug_info.scope_line == 22 + case "pytket_bar_load": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 28 + assert debug_info.scope_line is None + case "pytket_bar_stub": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 32 + assert debug_info.scope_line is None + case "": + # No metadata on the inner circuit function. + assert HugrDebugInfo not in func.metadata case _: raise AssertionError(f"Unexpected function name {op.f_name}") @@ -77,28 +110,62 @@ def foo() -> None: def test_call_location(): @guppy def foo() -> None: - bar() + bar() # call 1 + comptime_bar() # call 2 + q = qubit() # compiles to extension op (see test below) + pytket_bar_load(q) # call 3 + inner circuit function call 4 + discard(q) # compiles to extension op (see test below) hugr = foo.compile().modules[0] - for node, node_data in hugr.nodes(): - if isinstance(node_data.op, Call): - assert HugrDebugInfo in node.metadata - debug_info = DILocation.from_json(node.metadata[HugrDebugInfo.KEY]) - assert debug_info.line_no == 79 + calls = [node for node, node_data in hugr.nodes() if isinstance(node_data.op, Call)] + assert len(calls) == 4 + lines = [] + for call in calls: + assert HugrDebugInfo in call.metadata + debug_info = DILocation.from_json(call.metadata[HugrDebugInfo.KEY]) + if debug_info.line_no == 28: + assert debug_info.column == 0 + else: assert debug_info.column == 8 + lines.append(debug_info.line_no) + assert lines == [113, 114, 28, 116] + +# TODO: Improve this test. +def test_ext_op_location(): + @guppy.struct + class MyStruct: + x: int -def test_custom_function(): @guppy def foo() -> None: - q = qubit() - state_result("tag", q) - discard(q) + MyStruct(1) # Defined through `custom_function` (`MakeTuple` node) + q = qubit() # Defined through `hugr_op` + arr = array(q) # Forces the use of various array extension ops + state_result("tag", arr) # Defined through `custom_function` (custom node) + discard_array(arr) # Defined through `hugr_op` hugr = foo.compile().modules[0] + # TODO: Figure out how to attach metadata to these nodes. + # TODO: Find other such limitations and add tests for them. + known_limitations = [ + "tket.bool.read", + "prelude.panic<[Type(Tuple(int<6>, Tuple(int<6>, int<6>, int<6>)))], []>", + "prelude.panic<[], [Type(Tuple(int<6>, Tuple(int<6>, int<6>, int<6>)))]>", + ] + found_annotated_tuples = [] for node, node_data in hugr.nodes(): - if isinstance(node_data.op, ExtOp) and "unpack" not in node_data.op.name(): + if ( + isinstance(node_data.op, ExtOp) + and node_data.op.name() not in known_limitations + ): assert HugrDebugInfo in node.metadata + debug_info = DILocation.from_json(node.metadata[HugrDebugInfo.KEY]) + # Check constructor is annotated. + if isinstance(node_data.op, MakeTuple) and HugrDebugInfo in node.metadata: + debug_info = DILocation.from_json(node.metadata[HugrDebugInfo.KEY]) + found_annotated_tuples.append(debug_info.line_no) + assert 142 in found_annotated_tuples def test_turn_off_debug_mode(): diff --git a/tests/resources/metadata_example.py b/tests/resources/metadata_example.py index 1b89ba117..75bb278e5 100644 --- a/tests/resources/metadata_example.py +++ b/tests/resources/metadata_example.py @@ -1,6 +1,9 @@ """File used to test the filename table in debug info metadata.""" +from pytket import Circuit + from guppylang import guppy +from guppylang.std.quantum import qubit @guppy @@ -12,3 +15,18 @@ def bar() -> None: @guppy.declare def baz() -> None: ... + + +@guppy.comptime +def comptime_bar() -> None: + pass + + +circ = Circuit(1) +circ.H(0) + +pytket_bar_load = guppy.load_pytket("pytket_bar_load", circ, use_arrays=False) + + +@guppy.pytket(circ) +def pytket_bar_stub(q1: qubit) -> None: ... From c2ba07ac3ddd274d448f044e6ef3e5a48166fa03 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Mon, 9 Mar 2026 13:37:56 +0000 Subject: [PATCH 12/17] Fix typing --- .../guppylang_internals/compiler/expr_compiler.py | 5 ++++- .../definition/pytket_circuits.py | 1 + .../src/guppylang_internals/metadata/common.py | 12 +++++++----- .../src/guppylang_internals/metadata/debug_info.py | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py index ad294b6c1..f2bfb19a5 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py +++ b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py @@ -771,8 +771,11 @@ def visit_Compare(self, node: ast.Compare) -> Wire: raise InternalGuppyError("Node should have been removed during type checking.") +P = TypeVar("P", bound=ops.DfParentOp) + + def add_op( - builder: DfBase[ops.DfParentOp], + builder: DfBase[P], op: ops.DataflowOp, /, *args: Wire, diff --git a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py index af670d44f..ec7d110bf 100644 --- a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py +++ b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py @@ -201,6 +201,7 @@ def compile_outer( if debug_mode_enabled(): # Function stub case. if self.defined_at is not None: + assert isinstance(self.defined_at, ast.FunctionDef) func_metadata = make_subprogram_record( self.defined_at, ctx, is_decl=True ) diff --git a/guppylang-internals/src/guppylang_internals/metadata/common.py b/guppylang-internals/src/guppylang_internals/metadata/common.py index c511019ef..1b3540a4c 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/common.py +++ b/guppylang-internals/src/guppylang_internals/metadata/common.py @@ -32,9 +32,9 @@ class FunctionMetadata: """Class for storing metadata to be attached to Hugr nodes during compilation.""" _node_metadata: dict[str, JsonType] = field(default_factory=dict) - _RESERVED_KEYS: ClassVar[frozenset[type[Metadata[Any]]]] = { - HugrDebugInfo, - MetadataMaxQubits, + _RESERVED_KEYS: ClassVar[set[str]] = { + HugrDebugInfo.KEY, + MetadataMaxQubits.KEY, } def as_dict(self) -> dict[str, JsonType]: @@ -52,11 +52,13 @@ def get_debug_info(self) -> DebugRecord | None: return DebugRecord.from_json(self._node_metadata.get(HugrDebugInfo.KEY)) def get_max_qubits(self) -> int | None: - return self._node_metadata.get(MetadataMaxQubits.KEY) + qubits = self._node_metadata.get(MetadataMaxQubits.KEY) + assert qubits is None or isinstance(qubits, int) + return qubits @classmethod def reserved_keys(cls) -> set[str]: - return {t.KEY for t in cls._RESERVED_KEYS} + return cls._RESERVED_KEYS def add_metadata( diff --git a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py index cd6573d20..7a6d1f20d 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py +++ b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py @@ -1,6 +1,7 @@ import ast from abc import ABC, abstractmethod from dataclasses import dataclass +from typing import cast from hugr.metadata import JsonType, Metadata @@ -49,8 +50,7 @@ def to_json(self) -> dict[str, JsonType]: return { "directory": self.directory, "filename": self.filename, - # TODO: Fix table conversion / typing. - "file_table": self.file_table, + "file_table": cast("list[JsonType]", self.file_table), } @classmethod From ee6eea657555c9a41fbbfb96eb451a93420102b8 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Mon, 9 Mar 2026 13:39:30 +0000 Subject: [PATCH 13/17] Formatting --- .../src/guppylang_internals/metadata/common.py | 2 +- .../guppylang_internals/std/_internal/compiler/list.py | 8 ++------ .../std/_internal/compiler/platform.py | 4 +--- .../src/guppylang_internals/std/_internal/debug.py | 1 + guppylang-internals/src/guppylang_internals/tys/qubit.py | 1 + tests/metadata/test_version_metadata.py | 1 + 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/metadata/common.py b/guppylang-internals/src/guppylang_internals/metadata/common.py index 1b3540a4c..37712bd6c 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/common.py +++ b/guppylang-internals/src/guppylang_internals/metadata/common.py @@ -2,7 +2,7 @@ from typing import Any, ClassVar from hugr.hugr.node_port import ToNode -from hugr.metadata import JsonType, Metadata +from hugr.metadata import JsonType from guppylang_internals.diagnostic import Fatal from guppylang_internals.error import GuppyError diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py index 7c11557c2..ce9eb82d6 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py @@ -130,9 +130,7 @@ def build_linear_getitem( elem_opt_ty = ht.Option(elem_ty) none = self.add_op(ops.Tag(0, elem_opt_ty)) idx = self.add_op(convert_itousize(), idx) - list_wire, result = self.add_op( - list_set(elem_opt_ty), list_wire, idx, none - ) + list_wire, result = self.add_op(list_set(elem_opt_ty), list_wire, idx, none) elem_opt = build_unwrap_right(self.builder, result, "List index out of bounds") elem = build_unwrap( self.builder, elem_opt, "Linear list element has already been used" @@ -185,9 +183,7 @@ def build_linear_setitem( elem_opt_ty = ht.Option(elem_ty) elem = self.add_op(ops.Some(elem_ty), elem) idx = self.add_op(convert_itousize(), idx) - list_wire, result = self.add_op( - list_set(elem_opt_ty), list_wire, idx, elem - ) + list_wire, result = self.add_op(list_set(elem_opt_ty), list_wire, idx, elem) old_elem_opt = build_unwrap_right( self.builder, result, "List index out of bounds" ) diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py index 4dd8f0978..958838d7d 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py @@ -107,9 +107,7 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: arr = self.add_op(map_op, arr, array_read).out(0) hugr_elem_ty = tys.Bool # Turn `borrow_array` into regular `array` - arr = self.add_op(array_to_std_array(hugr_elem_ty, hugr_size), arr).out( - 0 - ) + arr = self.add_op(array_to_std_array(hugr_elem_ty, hugr_size), arr).out(0) hugr_ty = hugr.std.collections.array.Array(hugr_elem_ty, hugr_size) sig = tys.FunctionType(input=[hugr_ty], output=[]) diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/debug.py b/guppylang-internals/src/guppylang_internals/std/_internal/debug.py index 62034e8ae..fff28289c 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/debug.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/debug.py @@ -62,6 +62,7 @@ def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: raise GuppyTypeError(self.MissingQubitsError(self.node)) from guppylang.defs import GuppyDefinition + from guppylang.std.quantum import qubit assert isinstance(qubit, GuppyDefinition) diff --git a/guppylang-internals/src/guppylang_internals/tys/qubit.py b/guppylang-internals/src/guppylang_internals/tys/qubit.py index a24c49e9b..30f51d547 100644 --- a/guppylang-internals/src/guppylang_internals/tys/qubit.py +++ b/guppylang-internals/src/guppylang_internals/tys/qubit.py @@ -16,6 +16,7 @@ def qubit_ty() -> Type: it might result in circular imports. """ from guppylang.defs import GuppyDefinition + from guppylang.std.quantum import qubit assert isinstance(qubit, GuppyDefinition) diff --git a/tests/metadata/test_version_metadata.py b/tests/metadata/test_version_metadata.py index c8b26e047..7bb4c2da8 100644 --- a/tests/metadata/test_version_metadata.py +++ b/tests/metadata/test_version_metadata.py @@ -26,6 +26,7 @@ def foo() -> None: def test_used_extensions_computed_dynamically(): """Test that used extensions are computed based on actual usage.""" from guppylang.std.builtins import owned + from guppylang.std.quantum import h, qubit # A simple function with no special ops should only have minimal extensions From ab80d965ca1f7df40e138b3de6dda6d088439021 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Mon, 9 Mar 2026 13:42:03 +0000 Subject: [PATCH 14/17] Fix comment --- .../src/guppylang_internals/definition/pytket_circuits.py | 1 - 1 file changed, 1 deletion(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py index ec7d110bf..99ef62a5f 100644 --- a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py +++ b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py @@ -197,7 +197,6 @@ def compile_outer( outer_func = module.module_root_builder().define_function( self.name, func_type.body.input, func_type.body.output ) - # Mark both inner and outer function with the same location metadata. if debug_mode_enabled(): # Function stub case. if self.defined_at is not None: From ea73ac43a5a6f9eb8d5bfbff0a0652d6381b0955 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Mon, 9 Mar 2026 15:18:19 +0000 Subject: [PATCH 15/17] Try fixing imports --- .../src/guppylang_internals/definition/pytket_circuits.py | 1 - .../src/guppylang_internals/std/_internal/compiler/array.py | 3 ++- .../src/guppylang_internals/std/_internal/debug.py | 1 - tests/metadata/test_debug_info.py | 4 ++-- tests/metadata/test_version_metadata.py | 1 - tests/resources/metadata_example.py | 2 +- 6 files changed, 5 insertions(+), 7 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py index 99ef62a5f..1bc0bb645 100644 --- a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py +++ b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py @@ -421,7 +421,6 @@ def _signature_from_circuit( """Helper function for inferring a function signature from a pytket circuit.""" # May want to set proper unitary flags in the future. from guppylang.std.angles import angle # Avoid circular imports - from guppylang.std.quantum import qubit assert isinstance(qubit, GuppyDefinition) diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py index 4863938d4..3b1726b48 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py @@ -9,7 +9,6 @@ from hugr import tys as ht from hugr.std.collections.borrow_array import EXTENSION -from guppylang_internals.ast_util import AstNode from guppylang_internals.definition.custom import CustomCallCompiler from guppylang_internals.definition.value import CallReturnWires from guppylang_internals.error import InternalGuppyError @@ -23,6 +22,8 @@ if TYPE_CHECKING: from hugr.build.dfg import DfBase + from guppylang_internals.ast_util import AstNode + # ------------------------------------------------------ # --------------- std.array operations ----------------- diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/debug.py b/guppylang-internals/src/guppylang_internals/std/_internal/debug.py index fff28289c..62034e8ae 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/debug.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/debug.py @@ -62,7 +62,6 @@ def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: raise GuppyTypeError(self.MissingQubitsError(self.node)) from guppylang.defs import GuppyDefinition - from guppylang.std.quantum import qubit assert isinstance(qubit, GuppyDefinition) diff --git a/tests/metadata/test_debug_info.py b/tests/metadata/test_debug_info.py index 1e32fc97e..d030be985 100644 --- a/tests/metadata/test_debug_info.py +++ b/tests/metadata/test_debug_info.py @@ -1,4 +1,6 @@ +from guppylang.std import array from guppylang.std.debug import state_result +from guppylang.std.quantum import discard, discard_array, qubit from guppylang_internals.debug_mode import turn_off_debug_mode, turn_on_debug_mode from guppylang_internals.metadata.debug_info import ( DICompileUnit, @@ -9,8 +11,6 @@ from hugr.ops import Call, ExtOp, FuncDecl, FuncDefn, MakeTuple from guppylang import guppy -from guppylang.std import array -from guppylang.std.quantum import discard, discard_array, qubit from tests.resources.metadata_example import ( bar, baz, diff --git a/tests/metadata/test_version_metadata.py b/tests/metadata/test_version_metadata.py index 7bb4c2da8..c8b26e047 100644 --- a/tests/metadata/test_version_metadata.py +++ b/tests/metadata/test_version_metadata.py @@ -26,7 +26,6 @@ def foo() -> None: def test_used_extensions_computed_dynamically(): """Test that used extensions are computed based on actual usage.""" from guppylang.std.builtins import owned - from guppylang.std.quantum import h, qubit # A simple function with no special ops should only have minimal extensions diff --git a/tests/resources/metadata_example.py b/tests/resources/metadata_example.py index 75bb278e5..619dbc0cb 100644 --- a/tests/resources/metadata_example.py +++ b/tests/resources/metadata_example.py @@ -1,9 +1,9 @@ """File used to test the filename table in debug info metadata.""" +from guppylang.std.quantum import qubit from pytket import Circuit from guppylang import guppy -from guppylang.std.quantum import qubit @guppy From 8cb8c58300e77b54a06ea29404550febb8bb4462 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Mon, 9 Mar 2026 15:20:20 +0000 Subject: [PATCH 16/17] One more import error fix --- guppylang-internals/src/guppylang_internals/tys/qubit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/guppylang-internals/src/guppylang_internals/tys/qubit.py b/guppylang-internals/src/guppylang_internals/tys/qubit.py index 30f51d547..a24c49e9b 100644 --- a/guppylang-internals/src/guppylang_internals/tys/qubit.py +++ b/guppylang-internals/src/guppylang_internals/tys/qubit.py @@ -16,7 +16,6 @@ def qubit_ty() -> Type: it might result in circular imports. """ from guppylang.defs import GuppyDefinition - from guppylang.std.quantum import qubit assert isinstance(qubit, GuppyDefinition) From c77239694c98c2e46a45f65d6b9e283bd1448473 Mon Sep 17 00:00:00 2001 From: Tatiana S Date: Fri, 20 Mar 2026 12:19:30 +0000 Subject: [PATCH 17/17] Pin to hugr branch with moved specification --- .../compiler/expr_compiler.py | 3 +- .../definition/declaration.py | 2 +- .../definition/function.py | 8 +- .../definition/pytket_circuits.py | 9 +- .../guppylang_internals/definition/traced.py | 6 +- .../src/guppylang_internals/engine.py | 5 +- .../guppylang_internals/metadata/common.py | 5 +- .../metadata/debug_info.py | 137 +----------------- .../metadata/max_qubits.py | 3 +- pyproject.toml | 2 +- tests/metadata/test_debug_info.py | 16 +- uv.lock | 35 +---- 12 files changed, 29 insertions(+), 202 deletions(-) diff --git a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py index f2bfb19a5..a88d8d9d5 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py +++ b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py @@ -15,6 +15,7 @@ from hugr.build import function as hf from hugr.build.cond_loop import Conditional from hugr.build.dfg import DP, DfBase +from hugr.metadata import HugrDebugInfo from guppylang_internals.ast_util import AstNode, AstVisitor, get_file, get_type from guppylang_internals.cfg.builder import tmp_vars @@ -38,7 +39,7 @@ ) from guppylang_internals.engine import ENGINE from guppylang_internals.error import GuppyError, InternalGuppyError -from guppylang_internals.metadata.debug_info import HugrDebugInfo, make_location_record +from guppylang_internals.metadata.debug_info import make_location_record from guppylang_internals.nodes import ( AbortExpr, AbortKind, diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index 7ac9c9c0b..a26c46b65 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -5,6 +5,7 @@ from hugr import Node, Wire from hugr.build import function as hf from hugr.build.dfg import DefinitionBuilder, OpVar +from hugr.metadata import HugrDebugInfo from guppylang_internals.ast_util import ( AstNode, @@ -42,7 +43,6 @@ ) from guppylang_internals.diagnostic import Error from guppylang_internals.error import GuppyError -from guppylang_internals.metadata.debug_info import HugrDebugInfo from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.tys.param import Parameter diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 673829a39..c4dd30118 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -8,7 +8,9 @@ import hugr.tys as ht from hugr import Node, Wire from hugr.build.dfg import DefinitionBuilder, OpVar +from hugr.debug_info import DISubprogram from hugr.hugr.node_port import ToNode +from hugr.metadata import HugrDebugInfo from guppylang_internals.ast_util import ( AstNode, @@ -53,11 +55,7 @@ from guppylang_internals.engine import DEF_STORE, ENGINE from guppylang_internals.error import GuppyError from guppylang_internals.metadata.common import FunctionMetadata, add_metadata -from guppylang_internals.metadata.debug_info import ( - DISubprogram, - HugrDebugInfo, - make_location_record, -) +from guppylang_internals.metadata.debug_info import make_location_record from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap, to_span from guppylang_internals.tys.subst import Inst, Subst diff --git a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py index bba8b8bc9..4355232e3 100644 --- a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py +++ b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py @@ -7,7 +7,9 @@ from hugr import Node, Wire, envelope, ops, val from hugr import tys as ht from hugr.build.dfg import DefinitionBuilder, OpVar +from hugr.debug_info import DILocation, DISubprogram from hugr.envelope import EnvelopeConfig +from hugr.metadata import HugrDebugInfo from hugr.std.float import FLOAT_T from pytket.circuit import Circuit from tket.circuit import Tk2Circuit @@ -42,12 +44,7 @@ ) from guppylang_internals.engine import ENGINE from guppylang_internals.error import GuppyError, InternalGuppyError -from guppylang_internals.metadata.debug_info import ( - DILocation, - DISubprogram, - HugrDebugInfo, - make_location_record, -) +from guppylang_internals.metadata.debug_info import make_location_record from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap, Span, ToSpan from guppylang_internals.std._internal.compiler.array import ( diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index 39e93fc5b..41b9eac4e 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -7,6 +7,7 @@ import hugr.tys as ht from hugr import Node, Wire from hugr.build.dfg import DefinitionBuilder, OpVar +from hugr.metadata import HugrDebugInfo from guppylang_internals.ast_util import AstNode, with_loc from guppylang_internals.checker.core import Context, Globals @@ -35,10 +36,7 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import GuppyError -from guppylang_internals.metadata.debug_info import ( - HugrDebugInfo, - make_location_record, -) +from guppylang_internals.metadata.debug_info import make_location_record from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.tys.subst import Inst, Subst diff --git a/guppylang-internals/src/guppylang_internals/engine.py b/guppylang-internals/src/guppylang_internals/engine.py index d3b974247..f925cafd1 100644 --- a/guppylang-internals/src/guppylang_internals/engine.py +++ b/guppylang-internals/src/guppylang_internals/engine.py @@ -6,9 +6,10 @@ import hugr import hugr.build.function as hf from hugr import ops +from hugr.debug_info import DICompileUnit from hugr.envelope import ExtensionDesc, GeneratorDesc from hugr.ext import Extension, ExtensionRegistry -from hugr.metadata import HugrGenerator, HugrUsedExtensions +from hugr.metadata import HugrDebugInfo, HugrGenerator, HugrUsedExtensions from hugr.package import ModulePointer, Package from semver import Version @@ -30,8 +31,6 @@ ) from guppylang_internals.error import pretty_errors from guppylang_internals.metadata.debug_info import ( - DICompileUnit, - HugrDebugInfo, StringTable, ) from guppylang_internals.span import SourceMap diff --git a/guppylang-internals/src/guppylang_internals/metadata/common.py b/guppylang-internals/src/guppylang_internals/metadata/common.py index 37712bd6c..b4831e2a6 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/common.py +++ b/guppylang-internals/src/guppylang_internals/metadata/common.py @@ -1,12 +1,13 @@ from dataclasses import dataclass, field from typing import Any, ClassVar +from hugr.debug_info import DebugRecord from hugr.hugr.node_port import ToNode -from hugr.metadata import JsonType +from hugr.metadata import HugrDebugInfo +from hugr.utils import JsonType from guppylang_internals.diagnostic import Fatal from guppylang_internals.error import GuppyError -from guppylang_internals.metadata.debug_info import DebugRecord, HugrDebugInfo from guppylang_internals.metadata.max_qubits import MetadataMaxQubits diff --git a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py index 7a6d1f20d..f87a0bbc2 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py +++ b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py @@ -1,146 +1,11 @@ import ast -from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import cast -from hugr.metadata import JsonType, Metadata +from hugr.debug_info import DILocation from guppylang_internals.span import to_span -@dataclass -class DebugRecord(ABC): - """Abstract base class for debug records.""" - - @abstractmethod - def to_json(self) -> JsonType: - """Encodes the record as a dictionary of native types that can be serialized by - `json.dump`. - """ - - @classmethod - @abstractmethod - def from_json(cls, value: JsonType) -> "DebugRecord": - """Decodes the extension from a native types obtained from `json.load`.""" - - -class HugrDebugInfo(Metadata[DebugRecord]): - """Metadata storing debug information for a node.""" - - KEY = "core.debug_info" - - @classmethod - def to_json(cls, value: DebugRecord) -> JsonType: - return value.to_json() - - @classmethod - def from_json(cls, value: JsonType) -> DebugRecord: - return DebugRecord.from_json(value) - - -@dataclass -class DICompileUnit(DebugRecord): - """Debug information for a compilation unit, corresponds to a module node.""" - - directory: str - filename: int # File that contains Hugr entrypoint. - file_table: list[str] # Global table of all files referenced in the module. - - def to_json(self) -> dict[str, JsonType]: - return { - "directory": self.directory, - "filename": self.filename, - "file_table": cast("list[JsonType]", self.file_table), - } - - @classmethod - def from_json(cls, value: JsonType) -> "DICompileUnit": - if not isinstance(value, dict): - msg = f"Expected a dictionary for DICompileUnit, but got {type(value)}" - raise TypeError(msg) - for key in ("directory", "filename", "file_table"): - if key not in value: - msg = f"Expected DICompileUnit to have a '{key}' key but got {value}" - raise TypeError(msg) - files = value["file_table"] - if not isinstance(files, list): - msg = f"Expected 'file_table' to be a list but got {type(files)}" - raise TypeError(msg) - return DICompileUnit( - directory=str(value["directory"]), - filename=int(value["filename"]), - file_table=list[str](value["file_table"]), - ) - - -@dataclass -class DISubprogram(DebugRecord): - """Debug information for a subprogram, corresponds to a function definition or - declaration node.""" - - file: int # Index into the string table for filenames. - line_no: int # First line of the function definition. - scope_line: int | None = None # First line of the function body. - - def to_json(self) -> dict[str, str]: - return ( - { - "file": str(self.file), - "line_no": str(self.line_no), - "scope_line": str(self.scope_line), - } - # Declarations have no function body so could have no scope_line. - if self.scope_line is not None - else { - "file": str(self.file), - "line_no": str(self.line_no), - } - ) - - @classmethod - def from_json(cls, value: JsonType) -> "DISubprogram": - if not isinstance(value, dict): - msg = f"Expected a dictionary for DISubprogram, but got {type(value)}" - raise TypeError(msg) - for key in ("file", "line_no"): - if key not in value: - msg = f"Expected DISubprogram to have a '{key}' key but got {value}" - raise TypeError(msg) - # Declarations have no function body so could have no scope_line. - scope_line = int(value["scope_line"]) if "scope_line" in value else None - return DISubprogram( - file=int(value["file"]), - line_no=int(value["line_no"]), - scope_line=scope_line, - ) - - -@dataclass -class DILocation(DebugRecord): - """Debug information for a location, corresponds to call or extension operation - node.""" - - column: int - line_no: int - - def to_json(self) -> dict[str, str]: - return { - "column": str(self.column), - "line_no": str(self.line_no), - } - - @classmethod - def from_json(cls, value: JsonType) -> "DILocation": - if not isinstance(value, dict): - msg = f"Expected a dictionary for DILocation, but got {type(value)}" - raise TypeError(msg) - for key in ("column", "line_no"): - if key not in value: - msg = f"Expected DILocation to have a '{key}' key but got {value}" - raise TypeError(msg) - return DILocation(column=int(value["column"]), line_no=int(value["line_no"])) - - def make_location_record(node: ast.AST) -> DILocation: """Creates a DILocation metadata record for `node`.""" return DILocation( diff --git a/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py b/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py index f2243a66a..06ae61934 100644 --- a/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py +++ b/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py @@ -1,6 +1,7 @@ from dataclasses import dataclass -from hugr.metadata import JsonType, Metadata +from hugr.metadata import Metadata +from hugr.utils import JsonType @dataclass(frozen=True) diff --git a/pyproject.toml b/pyproject.toml index 2e8556c11..4b80f2ba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ guppylang = { workspace = true } guppylang-internals = { workspace = true } miette-py = { workspace = true } # Uncomment these to test the latest dependency version during development -# hugr = { git = "https://github.com/quantinuum/hugr", subdirectory = "hugr-py", rev = "191c473" } +hugr = { git = "https://github.com/quantinuum/hugr", subdirectory = "hugr-py", rev = "6b0358e" } [build-system] requires = ["hatchling"] diff --git a/tests/metadata/test_debug_info.py b/tests/metadata/test_debug_info.py index c5788ee23..7501e3c82 100644 --- a/tests/metadata/test_debug_info.py +++ b/tests/metadata/test_debug_info.py @@ -2,12 +2,8 @@ from guppylang.std.debug import state_result from guppylang.std.quantum import discard, discard_array, qubit from guppylang_internals.debug_mode import turn_off_debug_mode, turn_on_debug_mode -from guppylang_internals.metadata.debug_info import ( - DICompileUnit, - DILocation, - DISubprogram, - HugrDebugInfo, -) +from hugr.debug_info import DICompileUnit, DILocation, DISubprogram +from hugr.metadata import HugrDebugInfo from hugr.ops import Call, ExtOp, FuncDecl, FuncDefn, MakeTuple from guppylang import guppy @@ -72,8 +68,8 @@ def foo() -> None: assert HugrDebugInfo in func.metadata debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) assert debug_info.file == 0 - assert debug_info.line_no == 48 - assert debug_info.scope_line == 49 + assert debug_info.line_no == 45 + assert debug_info.scope_line == 46 case "bar": assert HugrDebugInfo in func.metadata debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) @@ -132,7 +128,7 @@ def foo() -> None: else: assert debug_info.column == 8 lines.append(debug_info.line_no) - assert lines == [116, 117, 28, 119] + assert lines == [113, 114, 28, 116] # TODO: Improve this test. @@ -169,7 +165,7 @@ def foo() -> None: if isinstance(node_data.op, MakeTuple) and HugrDebugInfo in node.metadata: debug_info = DILocation.from_json(node.metadata[HugrDebugInfo.KEY]) found_annotated_tuples.append(debug_info.line_no) - assert 145 in found_annotated_tuples + assert 142 in found_annotated_tuples def test_turn_off_debug_mode(): diff --git a/uv.lock b/uv.lock index 439b1e8a3..db3a6ebc4 100644 --- a/uv.lock +++ b/uv.lock @@ -974,7 +974,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "hugr", specifier = "~=0.15.4" }, + { name = "hugr", git = "https://github.com/quantinuum/hugr?subdirectory=hugr-py&rev=6b0358e" }, { name = "pytket", specifier = ">=1.34" }, { name = "tket", specifier = ">=0.12.7" }, { name = "tket-exts", specifier = "~=0.12.0" }, @@ -985,8 +985,8 @@ provides-extras = ["pytket"] [[package]] name = "hugr" -version = "0.15.4" -source = { registry = "https://pypi.org/simple" } +version = "0.15.5" +source = { git = "https://github.com/quantinuum/hugr?subdirectory=hugr-py&rev=6b0358e#6b0358ee34258040c410b0446cec9a45e709c55f" } dependencies = [ { name = "graphviz" }, { name = "pydantic" }, @@ -994,35 +994,6 @@ dependencies = [ { name = "semver" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/fe/676058e746b7509d2c80123c22444d81e5f470b7bdcd2c1159185b9a4749/hugr-0.15.4.tar.gz", hash = "sha256:0a0d72daa37854dd933fcea7c4ee0c715c21efdf2365700762f9c6f57afc0c50", size = 1050567, upload-time = "2026-02-20T14:11:24.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/f1/fb73bdf8d8da5c01338785163b3de5331c8bc31f5f4a4410eabd1d1ea7c9/hugr-0.15.4-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9002ce346931e20240c14d2dccde3e6f7e51ec77834b444a9b1d8c70fd954415", size = 3716743, upload-time = "2026-02-20T14:11:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f2/a368acdebfec252c1301327fa50faf7306110437a5f8c5a73332141b83d9/hugr-0.15.4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:203f733efe157c9c43d0314e5e7afe723416a1ac9088b53ac4b907785219fc5a", size = 3313534, upload-time = "2026-02-20T14:11:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/49/f8/4efcca2432ce8dbb00471790967ae17aaad9af438bd162750edaca909a44/hugr-0.15.4-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bf973c130aff874008b912e17683e5cbfffdc9db2793515945c43aedbee41b1", size = 3642898, upload-time = "2026-02-20T14:10:34.487Z" }, - { url = "https://files.pythonhosted.org/packages/ff/11/8f52c403f85e13330d6adec274b1b3ad0f41a91542cf1d8364029997643e/hugr-0.15.4-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8d962f2bdad0fd265ae21470b28f595e652dafde60fb50af0d0c565cf5b3b3b", size = 3643046, upload-time = "2026-02-20T14:10:38.462Z" }, - { url = "https://files.pythonhosted.org/packages/81/b9/4d1bce1a9525428b51a4f41cb48d257bfff811488756a90026e0a4e18f73/hugr-0.15.4-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85d489ada2727c2cfcfccfd03d6b2dc007c76b2655425d30d6004d26abf2223c", size = 3909770, upload-time = "2026-02-20T14:10:49.763Z" }, - { url = "https://files.pythonhosted.org/packages/35/6d/eaef430e984f0ef715b5857e85ec1a347f837850a03a9c34f5ff08740cd3/hugr-0.15.4-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7614c5a66969ebea0c2eb5098f8dc985fbe7b9edc81e70cce98326bf0bf18c67", size = 4097902, upload-time = "2026-02-20T14:10:41.868Z" }, - { url = "https://files.pythonhosted.org/packages/df/e4/8f383056983052f0d729455bad4595c35014ce6903bfe9895693f0efc4ac/hugr-0.15.4-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6252da7f1dddc2d854420700fd1a5dd67f215c8c5182d469eedc023aa320484c", size = 4179798, upload-time = "2026-02-20T14:10:45.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/a8/b420dcbf6902637f68a5210635fc0cab3505605739c635ecf0cb60025098/hugr-0.15.4-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c025244e0e66ef7b18735ae31f2909d0553843375922ffe2afb4231f3271da", size = 3983149, upload-time = "2026-02-20T14:10:53.369Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3a/1e5af2a8a9521c3e5813f2269c088bbd0b2ca90b9db0ed8374f36c1dd0f4/hugr-0.15.4-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9cb1402a8d363c81f0f1d0831bb1951f534a666b79fa55b72be5416de9b34acc", size = 3853876, upload-time = "2026-02-20T14:10:56.595Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0e/d7b3954c306d38cc86ca9af8f0962c3fe63ed38cbf9e56ba7a5075ebbdc1/hugr-0.15.4-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:392c7c316a521129ff38414ed779a9b129517b10c733e8936f167901fce43f0f", size = 3921103, upload-time = "2026-02-20T14:10:59.967Z" }, - { url = "https://files.pythonhosted.org/packages/c6/9f/3c66b77328dc46cb4c3d3df0f4cbfb7733346774e7a125525f23d8fddfbc/hugr-0.15.4-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bccfd6293928924dbcd65793f72f958b1db73785924fdf921dcd48632efe4345", size = 3996237, upload-time = "2026-02-20T14:11:03.751Z" }, - { url = "https://files.pythonhosted.org/packages/f8/3a/0cddb1f0d5ccefa2130f1aa03f592c4f65fd65d5bf0ae4268a007fbfeba9/hugr-0.15.4-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8c4e6d9ef297849c46fd9827db48b3e768b041c96e4ccb4ed036cdfa3b69a055", size = 4218977, upload-time = "2026-02-20T14:11:10.917Z" }, - { url = "https://files.pythonhosted.org/packages/e3/3a/abaca253c27a7fda9078002917649a2b431114b83d7a07c37b3bd737e12e/hugr-0.15.4-cp310-abi3-win32.whl", hash = "sha256:98a633f30cb3334786ad847465670df44c27bd5c8b16d81f45d7ba7e9925ea59", size = 3252870, upload-time = "2026-02-20T14:11:15.821Z" }, - { url = "https://files.pythonhosted.org/packages/f7/46/f67200d556b36f321e240cd82cf79ac27e8208698d2aef192dbc376c08b5/hugr-0.15.4-cp310-abi3-win_amd64.whl", hash = "sha256:ec416a0bf673a67efe52d2b8e7912921839cffb797f11ac79ab360cac4bee2ce", size = 3553381, upload-time = "2026-02-20T14:11:14.216Z" }, - { url = "https://files.pythonhosted.org/packages/b5/91/3f24f7d9af4ac945ba96ec4fa0174891d220a14de0f01dec25b52617ee0f/hugr-0.15.4-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:efdc2521d62854cb1b410fc58fd0cdcdb4b7a0fc0ad6541402aee129792eea37", size = 3715189, upload-time = "2026-02-20T14:11:22.574Z" }, - { url = "https://files.pythonhosted.org/packages/56/6c/f7d6be5911299f20c26a3bab5489fdf60cd2249e754a8b18e3d8955d0a83/hugr-0.15.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:00c79ba03ebd93cd2930452adfe096396849f54ecb7f249dd3da6e75bf5593b6", size = 3309749, upload-time = "2026-02-20T14:11:19.07Z" }, - { url = "https://files.pythonhosted.org/packages/a3/1a/72050d4744ba97ddeaa5b073eb9489d680b9838667696f7f83e4c11196fe/hugr-0.15.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:685c2ca64b2a1dad94dc025501bb93a9742cc4dc97d1886fa65e0eed33c8c607", size = 3643638, upload-time = "2026-02-20T14:10:36.639Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c7/db23b9364c84f72d9d950afaaabe319b5eb5ed3fe36ca6b66b7a7531c0cd/hugr-0.15.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9e57da8071a987be37fbe294a8049b8ce7d61494c4e1b7ec8cfafd5f6894896", size = 3644120, upload-time = "2026-02-20T14:10:40.076Z" }, - { url = "https://files.pythonhosted.org/packages/92/71/3ded41c860f90b00353894330e196c1b9fc4a89a5625cc538ae0ed5bcb7e/hugr-0.15.4-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:731e4405832aa4f5be8dd5e30a853ebbd88d9ac9e50f6ca9e2a40690d771ed7a", size = 3909806, upload-time = "2026-02-20T14:10:51.786Z" }, - { url = "https://files.pythonhosted.org/packages/a3/38/cb749ba447e790b5e2afb96e6a66689c8a427c95f5cb9dcbf5ca056f99b2/hugr-0.15.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8056372332789d1328c25ba493b756dc6918c7ff93677637c1be0affb8c4675", size = 4096032, upload-time = "2026-02-20T14:10:43.358Z" }, - { url = "https://files.pythonhosted.org/packages/c2/de/96a83a31973027e1bf5f4ae51a9d20d0ec18644285375a1fc8160430bb2e/hugr-0.15.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f60bccf1d8a240d4e47337495e1c3796f7b4427598f327e9ebba823864933cb4", size = 4180673, upload-time = "2026-02-20T14:10:47.559Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ca/5888c6a6a3b1a67343bb863741e8f9f7faf82bc5b04a8e54afc8a99366c4/hugr-0.15.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6547b072bfb3fdc892992969669b9a1af7526365f9ba067d085a17bca4b9056b", size = 3982740, upload-time = "2026-02-20T14:10:54.914Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e2/c790d50e5bc909444ae372c80a788df817a91aa783851b7b95d475ebcc04/hugr-0.15.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e695834e99ddcf98465a5d5907c5877edd9bcd1c68f60372b770a75b838022bd", size = 3853146, upload-time = "2026-02-20T14:10:58.198Z" }, - { url = "https://files.pythonhosted.org/packages/87/a3/f6799c8380c495af3ebd1341becf53feb11721807fafa602ec4d848d24e4/hugr-0.15.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fde6826753c7b3e9581b92231890706a084f9be1d063620aa68a4869408b0df8", size = 3923421, upload-time = "2026-02-20T14:11:01.809Z" }, - { url = "https://files.pythonhosted.org/packages/1f/23/79429d327aca17b1074c394c082e18d0860e633d7d360a64e6a02f179273/hugr-0.15.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7f52df31ecab7e8aaaa28be47671e2fb9ebeaf2ea978665fb851e64e4460a1b7", size = 3995649, upload-time = "2026-02-20T14:11:07.565Z" }, - { url = "https://files.pythonhosted.org/packages/10/fe/83616826ab058c80d02c9e69b9330156713f4642403d83123d237e90d464/hugr-0.15.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a81eda22b9315906ef90fa73a9c4193fb5fd3485ea7a879f3c61a77e33c85450", size = 4217283, upload-time = "2026-02-20T14:11:12.667Z" }, -] [[package]] name = "identify"