-
Notifications
You must be signed in to change notification settings - Fork 30
docs: Document Guppy libraries #1552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mr/feat/compile-library
Are you sure you want to change the base?
Changes from all commits
af7a656
319877c
6e4f9c3
18f39f9
1ae986f
ebcb017
bd70682
17bda6b
5303842
1dd6653
42f709a
f3a54d5
ee67cc0
9b504cb
af04b70
7b79531
31c6d35
43d1c20
94e24ee
be02ee0
288e410
8439503
ff7df03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| build |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Configuration file for the Sphinx documentation builder. | ||
| # See https://www.sphinx-doc.org/en/master/usage/configuration.html | ||
|
|
||
| project = "Guppy Libraries" | ||
| copyright = "2026, Quantinuum" | ||
| author = "Quantinuum" | ||
|
|
||
| extensions = [ | ||
| "sphinx.ext.napoleon", | ||
| "sphinx.ext.autodoc", | ||
| "sphinx.ext.coverage", | ||
| "sphinx.ext.viewcode", | ||
| "sphinx.ext.intersphinx", | ||
| "sphinx_copybutton", | ||
| "myst_parser", | ||
| ] | ||
|
|
||
| html_theme = "furo" | ||
|
|
||
| html_title = "Guppy library docs" | ||
|
|
||
| html_theme_options = {} | ||
|
|
||
| html_static_path = [] | ||
|
|
||
| exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] | ||
|
|
||
| intersphinx_mapping = { | ||
| "python": ("https://docs.python.org/3/", None), | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Guppy Libraries Documentation | ||
|
|
||
| An introductory tale into how Guppy libraries (are supposed to) work, how to write them, and how to use them. | ||
|
|
||
| ```{note} | ||
| This docs collection concerns Guppy libraries usage for independently compiling a set of functions to a Hugr package, | ||
| which can be independently distributed and e.g. cached for optimization purposes. If you are just looking to open | ||
| source your Guppy code, the standard packaging mechanisms of Python suffice, and you can ignore this page. | ||
| ``` | ||
|
|
||
| ```{toctree} | ||
| :maxdepth: 2 | ||
|
|
||
| overview | ||
| proposals | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| # Overview | ||
|
|
||
| ## Defining a library | ||
|
|
||
| A Guppy library is, at its simplest, a collection of functions that are compiled together into a Hugr package. | ||
| In contrast to the usual executable Hugr packages (like those produced from compiling single entrypoint functions), such | ||
| library Hugr packages feature (usually multiple) *public* functions, possibly with arguments and non-``None`` return | ||
| types. | ||
|
|
||
| A developer may define several functions beyond this collection, for example for internally sharing functionality | ||
| between the public functions. Furthermore, developers may want to create different libraries / Hugr packages from a | ||
| single codebase, requiring to export different public functions in each such library. | ||
| In Guppy, a library is defined by calling the ``guppy.library(...)`` function, passing the functions to be exported as | ||
| public. | ||
| Any functions reachable via those functions are still included in the Hugr package, but with private visibility. | ||
|
|
||
| For example, a library may be defined as follows: | ||
|
|
||
| ```python | ||
| from guppylang import guppy | ||
|
|
||
|
|
||
| @guppy | ||
| def my_func() -> None: | ||
| pass | ||
|
|
||
|
|
||
| @guppy | ||
| def another_func(x: int) -> None: | ||
| pass | ||
|
|
||
|
|
||
| @guppy | ||
| def a_third_func(x: int) -> int: | ||
| return x + 1 | ||
|
|
||
|
|
||
| # Creates the library object, but does not compile it | ||
| library = guppy.library( | ||
| my_func, | ||
| another_func, | ||
| a_third_func, | ||
| ) | ||
| ``` | ||
|
|
||
| For private functions, the name their Hugr nodes receive is not important. | ||
| However, public functions need to have a stable (and unique) name for the Hugr to be valid and useful. | ||
| Guppy includes a default name inference mechanism based on the fully qualified name of the function (including any | ||
| parent types and the module path), but it is also possible to explicitly specify the name (e.g. to avoid conflicts, make | ||
| a backward-compatible change, or to provide a more user-friendly name) using the `link_name` argument of the `@guppy` | ||
| decorator: | ||
|
|
||
| ```python | ||
| from guppylang import guppy | ||
|
|
||
|
|
||
| @guppy(link_name="my.custom.link.name") | ||
| def my_func() -> None: | ||
| pass | ||
| ``` | ||
|
|
||
| The following are not yet supported: | ||
|
|
||
| - Exporting generic functions of any kind (see the [proposal](proposals/generic.md)) | ||
| - Automatically collecting functions to be included in a library (see the [proposal](proposals/collection.md)) | ||
|
|
||
| ## Compiling a library and creating stubs | ||
|
|
||
| Once a library object is created, it can be compiled into a Hugr package, which can be distributed and used | ||
| independently of the source code: | ||
|
|
||
| ```python | ||
| from guppylang import guppy | ||
| from hugr.package import Package | ||
|
|
||
| library = guppy.library(...) # As above | ||
|
|
||
| # Hugr package contains any specified functions as public, and otherwise reachable functions as private | ||
| hugr_package: Package = library.compile() | ||
| with open("library.hugr", "wb") as f: | ||
| f.write(hugr_package.to_bytes()) | ||
| ``` | ||
|
|
||
| However, this Hugr package does not expose an interface that can be programmed against by consumers of the library *in | ||
| Guppy*. For this, the library author must define *stubs* for all exposed functions, utilising `@guppy.declare(...)` | ||
| decorators. | ||
|
|
||
| For the example above, this would look like: | ||
|
|
||
| ```python | ||
| from guppylang import guppy | ||
|
|
||
|
|
||
| @guppy.declare | ||
| def my_func() -> None: ... | ||
|
|
||
|
|
||
| @guppy.declare | ||
| def another_func(x: int) -> None: ... | ||
|
|
||
|
|
||
| @guppy.declare | ||
| def a_third_func(x: int) -> int: ... | ||
| ``` | ||
|
|
||
| Creation of these stubs is a manual process; it is currently not possible to automatically generate stubs for a | ||
| library or validate that definitions faithfully implement their corresponding stubs (see the | ||
| [proposal](proposals/stubs.md) for both). Furthermore, it is currently not possible to define the stubs, and | ||
| subsequently reference the stubs in the library function definitions for easier consistency checks (see the | ||
| [proposal](proposals/impls.md) for that). | ||
|
|
||
| Finally, these stubs (importable, thus in `*.py` files instead of `*.pyi` as usual) should be distributed using regular | ||
| Python packaging mechanisms, so that users of the library can install and program against them. This distribution may or | ||
| may not contain the Hugr package as well. | ||
|
|
||
| ## Using a library | ||
|
|
||
| Let's call the library in the example above `super_guppy`. | ||
| That is, the library author has published a distribution containing the stubs above, to be imported `from super_guppy`. | ||
| A consumer of the library has installed that distribution using their favourite package manager (either from an index, | ||
| or by downloading the stub repository). | ||
|
|
||
| The consumer may now program against the API as follows: | ||
|
|
||
| ```python | ||
| from guppylang import guppy | ||
| from guppylang.std.builtins import result | ||
| from super_guppy import my_func, a_third_func | ||
|
|
||
|
|
||
| @guppy | ||
| def consumer_func() -> None: | ||
| my_func() | ||
| result("library_call", a_third_func(5)) | ||
|
|
||
|
|
||
| @guppy | ||
| def main() -> None: | ||
| consumer_func() | ||
| ``` | ||
|
|
||
| In this case, the consumer aims to create an executable Hugr package (e.g. by calling `main.compile()` and creating a | ||
| package with a single, argument-less entrypoint). However, the created Hugr package is incomplete: It lacks the function | ||
| bodies of the library functions, and thus cannot be executed. | ||
|
|
||
| As of https://github.com/Quantinuum/hugr/commit/329c243fffff0d6c4437664a012361619a0a425e, `hugr-py` provides the means | ||
| to link one or more library Hugr packages into the consumer Hugr package. This may look like: | ||
|
|
||
| ```python | ||
| from hugr.package import Package | ||
|
|
||
| package_a: Package = ... | ||
| package_b: Package = ... | ||
| package_c: Package = ... | ||
|
|
||
| package_d = package_a.link(package_b, package_c) | ||
|
|
||
| # Now package_d contains a single module with all the functions | ||
| # from package_a, package_b, and package_c | ||
| ``` | ||
|
|
||
| Whenever two or more modules are linked together, the resulting module will keep the *single* non-module entrypoint | ||
| across all modules, if it exists. When more than one module has a non-module entrypoint (i.e. more than one module is | ||
| executable), an error is raised. | ||
|
|
||
| There are no guarantees about the order of modules being linked together, or whether that is pairwise or all at once. | ||
| The user can exert a certain level of control by gradually linking packages together, using multiple calls to `link`. | ||
| Furthermore, future versions of `hugr-py` may provide even more control with extensions / alternatives to `link`. | ||
|
|
||
| For convenience, the library Hugr package may be provided to the consumer program executor, so that it can be | ||
| automatically linked in before compiling to binary. For example, using selene, this may look like: | ||
|
|
||
| ```python | ||
| main.emulator(n_qubits=0, libs=[lib_package]).with_shots(100).run() | ||
| ``` | ||
|
|
||
| Currently, Hugr packages have to be manually downloaded / imported from whatever distribution mechanism the library | ||
| author chose. In the future, library authors may opt to distribute Hugr packages in Python wheels as well, and have | ||
| consuming code auto-collect these from the Python environment (see the [proposal](proposals/discovery.md) for this). | ||
|
|
||
| ```{note} | ||
| A consumer of `super_guppy` may as well be another library author. Dependency of a library should be specified as usual | ||
| in Python requirements by depending on the header distributions (or through other common mechanisms). | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # Proposals | ||
|
|
||
| A collection of proposals for QOL features related to Guppy libraries. | ||
|
|
||
| ```{toctree} | ||
| :glob: | ||
|
|
||
| proposals/* | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # (Proposal) More ways to define a library / auto-collection | ||
|
|
||
| Currently, functions must be manually fed into `guppy.library(...)` to be included in the library. This proposal | ||
| concerns additional ways to define (members of) a library, and automatically collect functions to be included in it. | ||
|
|
||
| A potential addition would be a `@guppy(export=...)` argument, that registers the function to be included in *the* | ||
| library in some global Guppy store (like the `DEF_STORE`). A function could then be included simply by adding that | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a "global library" is a bit confusing - this still would require some notion of a specific library the function is exported in. The string method below addresses this, but passing in library objects rather than strings seems preferable
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems very similar to how GuppyModules were handled. |
||
| argument, e.g.: | ||
|
|
||
| ```python | ||
| from guppylang import guppy | ||
|
|
||
|
|
||
| @guppy(export=True) | ||
| def included() -> None: | ||
| ... | ||
|
|
||
|
|
||
| @guppy # or @guppy(export=False) for explicit exclusion | ||
| def not_included() -> None: | ||
| ... | ||
| ``` | ||
|
|
||
| This is similar to a `public` / `private` modifier, and indirectly determines the visibility of the function in the Hugr | ||
| package. It may be added to `@guppy.struct` and `@guppy.enum` as well, to include the functions on these constructs in | ||
| the library as public as well. | ||
|
|
||
| A call to `guppy.library` could then be simplified to an explicit opt-in to auto-collection: | ||
|
|
||
| ```python | ||
| from guppylang import guppy | ||
|
|
||
| # ... the functions above ... | ||
|
|
||
| # Includes `included`, but not `not_included` as a public member. | ||
| library = guppy.library(auto_collect=True) | ||
| ``` | ||
|
|
||
| ## Optional: Keys | ||
|
|
||
| In addition to supporting `True | False | None` for the `export` argument, it may be useful to support string keys as | ||
| well, to allow for more fine-grained control over the produced packages in which the function is included (concerning | ||
| both the visibility in the produced Hugr package and the stubs that are generated, e.g. | ||
| through [the stub proposal](stubs.md)). | ||
|
|
||
| For example, one may want to include certain (core) functions in all versions of the library, but specify certain sets | ||
| of functions to be included (akin to `extras` from Python packages): | ||
|
|
||
| ```python | ||
| from guppylang import guppy | ||
|
|
||
|
|
||
| @guppy(export=True) | ||
| def included_in_all_libs() -> None: | ||
| ... | ||
|
|
||
|
|
||
| @guppy(export='key1') | ||
| def included_when_key_1() -> None: | ||
| ... | ||
|
|
||
|
|
||
| @guppy(export='key2') | ||
| def included_when_key_2() -> None: | ||
| ... | ||
|
|
||
|
|
||
| @guppy(export='key3') | ||
| def included_when_key_3() -> None: | ||
| ... | ||
| ``` | ||
|
|
||
| Auto-collection may then take a range of keys to include: | ||
|
|
||
| ```python | ||
| from guppylang import guppy | ||
|
|
||
| # ... the functions above ... | ||
|
|
||
| # Includes all functions with export=True, export='key1', and export='key2', but not export='key3' | ||
| library = guppy.library(auto_collect=['key1', 'key2']) | ||
| ``` | ||
Uh oh!
There was an error while loading. Please reload this page.