Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
1 change: 1 addition & 0 deletions docs/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ mkdir build
touch build/.nojekyll # Disable jekyll to keep files starting with underscores

uv run --group docs sphinx-build -b html ./api-docs ./build/api-docs
uv run --group docs sphinx-build -b html ./library-docs ./build/library-docs
Empty file added docs/library-docs/__init__.py
Empty file.
30 changes: 30 additions & 0 deletions docs/library-docs/conf.py
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),
}
16 changes: 16 additions & 0 deletions docs/library-docs/index.md
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
```
184 changes: 184 additions & 0 deletions docs/library-docs/overview.md
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).
```
9 changes: 9 additions & 0 deletions docs/library-docs/proposals.md
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/*
```
82 changes: 82 additions & 0 deletions docs/library-docs/proposals/collection.md
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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'])
```
Loading
Loading