diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml deleted file mode 100644 index 002c2fd2..00000000 --- a/.github/workflows/issue.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: New issue - -on: - issues: - types: [opened] - -jobs: - jira_task: - name: Create Jira issue - runs-on: ubuntu-22.04 - steps: - - name: Login - uses: atlassian/gajira-login@v3.0.1 - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - - name: Parse the priority label into a Jira priority - id: set_priority_var - env: - LABELS: ${{ join(github.event.issue.labels.*.name, ' ') }} - run: | - MY_RESULT="" - for label in $LABELS - do - case $label in - P-low) - MY_RESULT=Low - break;; - P-medium) - MY_RESULT=Medium - break;; - P-high) - MY_RESULT=High - break;; - P-critical) - MY_RESULT=Highest - break;; - esac - done - if [ ! -z $MY_RESULT ] - then - MY_RESULT=", \"priority\": { \"name\": \"$MY_RESULT\" }" - fi - echo "JIRA_PRIORITY_FIELD=$MY_RESULT" >> $GITHUB_OUTPUT - - - name: Create Bug - uses: atlassian/gajira-create@v3.0.1 - if: ${{ contains(github.event.issue.labels.*.name, 'bug') }} - env: - JIRA_PRIORITY_FIELD: ${{ steps.set_priority_var.outputs.JIRA_PRIORITY_FIELD }} - with: - project: TKET - issuetype: Bug - summary: ยซ [guppy] ${{ github.event.issue.title }}ยป - description: ${{ github.event.issue.html_url }} - fields: '{ "labels": ["guppy"] ${{ env.JIRA_PRIORITY_FIELD }} }' - - - name: Create Task - uses: atlassian/gajira-create@v3.0.1 - if: ${{ ! contains(github.event.issue.labels.*.name, 'bug') }} - env: - JIRA_PRIORITY_FIELD: ${{ steps.set_priority_var.outputs.JIRA_PRIORITY_FIELD }} - with: - project: TKET - issuetype: Task - summary: ยซ [guppy] ${{ github.event.issue.title }}ยป - description: ${{ github.event.issue.html_url }} - fields: '{ "labels": ["guppy"] ${{ env.JIRA_PRIORITY_FIELD }} }' diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 33cb9674..d33312f9 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -14,7 +14,7 @@ on: types: [checks_requested] permissions: - pull-requests: read + pull-requests: write jobs: main: @@ -23,8 +23,15 @@ jobs: # The action does not support running on merge_group events, # but if the check succeeds in the PR there is no need to check it again. if: github.event_name == 'pull_request_target' + outputs: + # Whether the PR title indicates a breaking change. + breaking: ${{ steps.breaking.outputs.breaking }} + # Whether the PR body contains a "BREAKING CHANGE:" footer describing the breaking change. + has_breaking_footer: ${{ steps.breaking.outputs.has_breaking_footer }} steps: - - uses: amannn/action-semantic-pull-request@v5 + - name: Validate the PR title format + uses: amannn/action-semantic-pull-request@v5 + id: lint_pr_title env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -70,3 +77,87 @@ jobs: # event triggers in your workflow. ignoreLabels: | ignore-semantic-pull-request + + # `action-semantic-pull-request` does not parse the title, so it cannot + # detect if it is marked as a breaking change. + # + # Since at this point we know the PR title is a valid conventional commit, + # we can use a simple regex that looks for a '!:' sequence. It could be + # more complex, but we don't care about false positives. + - name: Check for breaking change flag + id: breaking + run: | + if [[ "${PR_TITLE}" =~ ^.*\!:.*$ ]]; then + echo "breaking=true" >> $GITHUB_OUTPUT + else + echo "breaking=false" >> $GITHUB_OUTPUT + fi + + # Check if the PR comment has a "BREAKING CHANGE:" footer describing + # the breaking change. + if [[ "${PR_BODY}" != *"BREAKING CHANGE:"* ]]; then + echo "has_breaking_footer=false" >> $GITHUB_OUTPUT + else + echo "has_breaking_footer=true" >> $GITHUB_OUTPUT + fi + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + + # Post a help comment if the PR title indicates a breaking change but does + # not contain a "BREAKING CHANGE:" footer. + - name: Require "BREAKING CHANGE:" footer for breaking changes + id: breaking-comment + if: ${{ steps.breaking.outputs.breaking == 'true' && steps.breaking.outputs.has_breaking_footer == 'false' }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-title-lint-error + message: | + Hey there and thank you for opening this pull request! ๐Ÿ‘‹๐Ÿผ + + It looks like your proposed title indicates a breaking change. If that's the case, + please make sure to include a "BREAKING CHANGE:" footer in the body of the pull request + describing the breaking change and any migration instructions. + GITHUB_TOKEN: ${{ secrets.HUGRBOT_PAT }} + - name: Fail if the footer is required but missing + if: ${{ steps.breaking.outputs.breaking == 'true' && steps.breaking.outputs.has_breaking_footer == 'false' }} + run: exit 1 + + - name: Post a comment if the PR badly formatted + uses: marocchino/sticky-pull-request-comment@v2 + # When the previous steps fails, the workflow would stop. By adding this + # condition you can continue the execution with the populated error message. + if: always() && (steps.lint_pr_title.outputs.error_message != null) + with: + header: pr-title-lint-error + message: | + Hey there and thank you for opening this pull request! ๐Ÿ‘‹๐Ÿผ + + We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) + and it looks like your proposed title needs to be adjusted. + + Your title should look like this. The scope field is optional. + ``` + (): + ``` + + If the PR includes a breaking change, mark it with an exclamation mark: + ``` + !: + ``` + and include a "BREAKING CHANGE:" footer in the body of the pull request. + + Details: + ``` + ${{ steps.lint_pr_title.outputs.error_message }} + ``` + GITHUB_TOKEN: ${{ secrets.HUGRBOT_PAT }} + + # Delete previous comments when the issues have been resolved + # This step doesn't run if any of the previous checks fails. + - name: Delete previous comments + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-title-lint-error + delete: true + GITHUB_TOKEN: ${{ secrets.HUGRBOT_PAT }} diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index 678a1517..c1f36c16 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -1,12 +1,22 @@ name: Build and publish python wheels +# Builds and publishes the wheels on pypi. +# +# When running on a push-to-main event, or as a workflow dispatch on a branch, +# this workflow will do a dry-run publish to test-pypi. +# +# When running on a release event or as a workflow dispatch for a tag, +# and if the tag matches `hugr-py-v*`, +# this workflow will publish the wheels to pypi. +# If the version is already published, pypi just ignores it. on: workflow_dispatch: push: branches: - main - tags: - - 'v**' + release: + types: + - published jobs: build-publish: @@ -26,7 +36,7 @@ jobs: cache: "poetry" - name: Build sdist and wheels - run: poetry build + run: poetry build -o dist - name: Upload the built packages as artifacts uses: actions/upload-artifact@v4 @@ -37,14 +47,23 @@ jobs: dist/*.whl - name: Publish to test instance of PyPI (dry-run) - if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref_type == 'branch' ) }} + if: | + ${{ github.event_name == 'push' && github.ref_type == 'branch' }} || + ${{ github.event_name == 'workflow_dispatch' && github.ref_type == 'branch'}} run: | + echo "Doing a dry-run publish to test-pypi..." + echo "Based on the following workflow variables, this is not a hugr-py version tag push:" + echo " - event_name: ${{ github.event_name }}" + echo " - ref_type: ${{ github.ref_type }}" + echo " - ref: ${{ github.ref }}" poetry config repositories.test-pypi https://test.pypi.org/legacy/ poetry config pypi-token.test-pypi ${{ secrets.PYPI_TEST_PUBLISH }} - poetry publish -r test-pypi --skip-existing --dry-run + poetry publish -r test-pypi --dist-dir dist --skip-existing --dry-run - name: Publish to PyPI - if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} + if: | + ${{ (github.event_name == 'release' && github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v'))}} || + ${{ (github.event_name == 'workflow_dispatch' && github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v'))}} || run: | poetry config pypi-token.pypi ${{ secrets.PYPI_PUBLISH }} - poetry publish --skip-existing + poetry publish --dist-dir dist --skip-existing diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 7d859422..16534f14 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,4 +1,8 @@ +# Automatic changelog and version bumping with release-please for python projects +name: Release-please ๐Ÿ + on: + workflow_dispatch: {} push: branches: - main @@ -7,13 +11,13 @@ permissions: contents: write pull-requests: write -name: release-please - jobs: release-please: + name: Create release PR runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@v4 + - uses: googleapis/release-please-action@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} - config-file: release-please-config.json + # Using a personal access token so releases created by this workflow can trigger the deployment workflow + token: ${{ secrets.HUGRBOT_PAT }} + config-file: release-please-config.json diff --git a/.gitignore b/.gitignore index 1fe6322b..f4e70187 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,4 @@ devenv.local.nix # pre-commit .pre-commit-config.yaml +/.envrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7fa21412..3422a4cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 # Use the ref you want to point at + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -20,24 +20,25 @@ repos: - id: debug-statements - repo: https://github.com/crate-ci/typos - rev: v1.19.0 + rev: v1.21.0 hooks: - id: typos - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.5 + rev: v0.4.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.8.0" + rev: "v1.10.0" hooks: - id: mypy pass_filenames: false args: [--package=guppylang] additional_dependencies: [ + hugr, graphviz, networkx, ormsgpack, diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6b7b74c5..258342d8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.0" -} \ No newline at end of file + ".": "0.5.2" +} diff --git a/.typos.toml b/.typos.toml index 92413257..03e5011d 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,3 +1,4 @@ [default.extend-words] -inot = "inot" fle = "fle" +ine = "ine" +inot = "inot" diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d9a70c..3e6a7a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [0.5.2](https://github.com/CQCL/guppylang/compare/v0.5.1...v0.5.2) (2024-06-13) + + +### Bug Fixes + +* Don't reorder inputs of entry BB ([#243](https://github.com/CQCL/guppylang/issues/243)) ([ad56b99](https://github.com/CQCL/guppylang/commit/ad56b991c03e1bc52690e41450521e7fe9100268)) + +## [0.5.1](https://github.com/CQCL/guppylang/compare/v0.5.0...v0.5.1) (2024-06-12) + + +### Bug Fixes + +* Serialisation of bool values ([#239](https://github.com/CQCL/guppylang/issues/239)) ([16a77db](https://github.com/CQCL/guppylang/commit/16a77dbd4c5905eff6c4ddabe66b5ef1b8a7e15b)) + +## [0.5.0](https://github.com/CQCL/guppylang/compare/v0.4.0...v0.5.0) (2024-06-10) + + +### Features + +* Add extern symbols ([#236](https://github.com/CQCL/guppylang/issues/236)) ([977ccd8](https://github.com/CQCL/guppylang/commit/977ccd831a3df1bdf49582309bce065a865d3e31)) + +## [0.4.0](https://github.com/CQCL/guppylang/compare/v0.3.0...v0.4.0) (2024-05-30) + + +### Features + +* Export py function ([6dca95d](https://github.com/CQCL/guppylang/commit/6dca95deda3cc5bd103df104e33991c9adce2be2)) + ## [0.3.0](https://github.com/CQCL/guppylang/compare/v0.2.0...v0.3.0) (2024-05-22) diff --git a/README.md b/README.md index aea04f95..53e4d16f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [codecov]: https://img.shields.io/codecov/c/gh/CQCL/guppylang?logo=codecov [py-version]: https://img.shields.io/pypi/pyversions/guppylang - [pypi]: https://img.shields.io/pypi/v/guppylang + [pypi]: https://img.shields.io/pypi/v/guppylang Guppy is a quantum programming language that is fully embedded into Python. It allows you to write high-level hybrid quantum programs with classical control flow and mid-circuit measurements using Pythonic syntax: diff --git a/devenv.lock b/devenv.lock index 39312958..55cf170e 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,11 +3,11 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1713955155, + "lastModified": 1717072927, "owner": "cachix", "repo": "devenv", - "rev": "fc9fc0dfc0b21a58fc7de9de11ad4f2bbbf9b076", - "treeHash": "24d3d253b35b868b1f1466c6c2b7aa81978a8114", + "rev": "fe3b1de09bcf45040a8bbfe850feba403a8af73f", + "treeHash": "920ef8f699e313c50d1cb8cd4567c9e833691a31", "type": "github" }, "original": { @@ -25,11 +25,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1713939967, + "lastModified": 1717136818, "owner": "nix-community", "repo": "fenix", - "rev": "5c3ff469526a6ca54a887fbda9d67aef4dd4a921", - "treeHash": "8fe5738bd85caabed2743fced62f69e13c8b851e", + "rev": "14c3b99d4b7cb91343807eac77f005ed9218f742", + "treeHash": "ab82469e3b2841ddca5099c8c32e84c9a5749b15", "type": "github" }, "original": { @@ -54,24 +54,6 @@ "type": "github" } }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1710146030, - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "treeHash": "bd263f021e345cb4a39d80c126ab650bebc3c10c", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "gitignore": { "inputs": { "nixpkgs": [ @@ -95,11 +77,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1713805509, + "lastModified": 1717112898, "owner": "NixOS", "repo": "nixpkgs", - "rev": "1e1dc66fe68972a76679644a5577828b6a7e8be4", - "treeHash": "45178d59be92731d710aea19878b2f10c925a040", + "rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", + "treeHash": "c025fb6c06ce8e6bafe5befc1fb801da72dd8df3", "type": "github" }, "original": { @@ -111,11 +93,11 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1713725259, + "lastModified": 1716991068, "owner": "NixOS", "repo": "nixpkgs", - "rev": "a5e4bbcb4780c63c79c87d29ea409abf097de3f7", - "treeHash": "d71e381aa8a18139cbba1b0bae4338a2b78108df", + "rev": "25cf937a30bf0801447f6bf544fc7486c6309234", + "treeHash": "67d550c2b5e96c6c1e2b317c54a37b4ca5fef106", "type": "github" }, "original": { @@ -128,7 +110,6 @@ "pre-commit-hooks": { "inputs": { "flake-compat": "flake-compat", - "flake-utils": "flake-utils", "gitignore": "gitignore", "nixpkgs": [ "nixpkgs" @@ -136,11 +117,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1713954846, + "lastModified": 1716213921, "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "6fb82e44254d6a0ece014ec423cb62d92435336f", - "treeHash": "a456512c8da29752b79131f1e5b45053e2394078", + "rev": "0e8fcc54b842ad8428c9e705cb5994eaf05c26a0", + "treeHash": "17b9f9f9983467bcff247b09761dca9831d3d3be", "type": "github" }, "original": { @@ -160,11 +141,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1713801366, + "lastModified": 1716828004, "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "e31c9f3fe11148514c3ad254b639b2ed7dbe35de", - "treeHash": "29bb80054fd850ae0830afaaeed46fbb4766f8df", + "rev": "b32f181f477576bb203879f7539608f3327b6178", + "treeHash": "4b8a01567abe7a8fc5561563aa52ac6c029fb31d", "type": "github" }, "original": { @@ -173,21 +154,6 @@ "repo": "rust-analyzer", "type": "github" } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "treeHash": "cce81f2a0f0743b2eb61bc2eb6c7adbe2f2c6beb", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/docs/api-docs/.gitignore b/docs/api-docs/.gitignore index df91f175..cd6936e2 100644 --- a/docs/api-docs/.gitignore +++ b/docs/api-docs/.gitignore @@ -1,3 +1,3 @@ _autosummary _build -generated \ No newline at end of file +generated diff --git a/docs/build.sh b/docs/build.sh index 66bde126..ff202cc9 100755 --- a/docs/build.sh +++ b/docs/build.sh @@ -1,3 +1,5 @@ +#!/bin/sh + mkdir build touch build/.nojekyll # Disable jekyll to keep files starting with underscores diff --git a/examples/1-Getting-Started.md b/examples/1-Getting-Started.md index 047f1a56..822fcd05 100644 --- a/examples/1-Getting-Started.md +++ b/examples/1-Getting-Started.md @@ -4,4 +4,3 @@ This file explains how you can get started using Guppy. TODO - diff --git a/guppylang/__init__.py b/guppylang/__init__.py index ca1a9acd..32189089 100644 --- a/guppylang/__init__.py +++ b/guppylang/__init__.py @@ -1,5 +1,5 @@ from guppylang.decorator import guppy from guppylang.module import GuppyModule from guppylang.prelude import builtins, quantum -from guppylang.prelude.builtins import Bool, Float, Int, List, linst +from guppylang.prelude.builtins import Bool, Float, Int, List, linst, py from guppylang.prelude.quantum import qubit diff --git a/guppylang/cfg/builder.py b/guppylang/cfg/builder.py index 60fc13c1..3e0a0b92 100644 --- a/guppylang/cfg/builder.py +++ b/guppylang/cfg/builder.py @@ -440,7 +440,9 @@ def visit_Compare( end_lineno=right.end_lineno, end_col_offset=right.end_col_offset, ) - for left, op, right in zip(comparators[:-1], node.ops, comparators[1:]) + for left, op, right in zip( + comparators[:-1], node.ops, comparators[1:], strict=True + ) ] conj = ast.BoolOp(op=ast.And(), values=values) set_location_from(conj, node) diff --git a/guppylang/checker/expr_checker.py b/guppylang/checker/expr_checker.py index f1c6e0ee..e0ee56b6 100644 --- a/guppylang/checker/expr_checker.py +++ b/guppylang/checker/expr_checker.py @@ -687,7 +687,7 @@ def type_check_args( check_num_args(len(func_ty.inputs), len(inputs), node) new_args: list[ast.expr] = [] - for inp, ty in zip(inputs, func_ty.inputs): + for inp, ty in zip(inputs, func_ty.inputs, strict=True): a, s = ExprChecker(ctx).check(inp, ty.substitute(subst), "argument") new_args.append(a) subst |= s diff --git a/guppylang/checker/func_checker.py b/guppylang/checker/func_checker.py index 5f2d52f6..7a979341 100644 --- a/guppylang/checker/func_checker.py +++ b/guppylang/checker/func_checker.py @@ -34,7 +34,7 @@ def check_global_func_def( cfg = CFGBuilder().build(func_def.body, returns_none, globals) inputs = [ Variable(x, ty, loc, None) - for x, ty, loc in zip(ty.input_names, ty.inputs, args) + for x, ty, loc in zip(ty.input_names, ty.inputs, args, strict=True) ] return check_cfg(cfg, inputs, ty.output, globals) @@ -88,7 +88,9 @@ def check_nested_func_def( # Construct inputs for checking the body CFG inputs = list(captured.values()) + [ Variable(x, ty, func_def.args.args[i], None) - for i, (x, ty) in enumerate(zip(func_ty.input_names, func_ty.inputs)) + for i, (x, ty) in enumerate( + zip(func_ty.input_names, func_ty.inputs, strict=True) + ) ] def_id = DefId.fresh() globals = ctx.globals diff --git a/guppylang/checker/stmt_checker.py b/guppylang/checker/stmt_checker.py index 272814c5..3025e781 100644 --- a/guppylang/checker/stmt_checker.py +++ b/guppylang/checker/stmt_checker.py @@ -71,7 +71,7 @@ def _check_assign(self, lhs: ast.expr, ty: Type, node: ast.stmt) -> None: f"(expected {n}, got {m})", node, ) - for pat, el_ty in zip(elts, tys): + for pat, el_ty in zip(elts, tys, strict=True): self._check_assign(pat, el_ty, node) # TODO: Python also supports assignments like `[a, b] = [1, 2]` or diff --git a/guppylang/compiler/cfg_compiler.py b/guppylang/compiler/cfg_compiler.py index 53de3da9..8e538970 100644 --- a/guppylang/compiler/cfg_compiler.py +++ b/guppylang/compiler/cfg_compiler.py @@ -28,17 +28,17 @@ def compile_cfg( blocks: dict[CheckedBB, CFNode] = {} for bb in cfg.bbs: - blocks[bb] = compile_bb(bb, graph, parent, globals) + blocks[bb] = compile_bb(bb, graph, parent, bb == cfg.entry_bb, globals) for bb in cfg.bbs: for succ in bb.successors: graph.add_edge(blocks[bb].add_out_port(), blocks[succ].in_port(None)) def compile_bb( - bb: CheckedBB, graph: Hugr, parent: Node, globals: CompiledGlobals + bb: CheckedBB, graph: Hugr, parent: Node, is_entry: bool, globals: CompiledGlobals ) -> CFNode: """Compiles a single basic block to Hugr.""" - inputs = sort_vars(bb.sig.input_row) + inputs = bb.sig.input_row if is_entry else sort_vars(bb.sig.input_row) # The exit BB is completely empty if len(bb.successors) == 0: diff --git a/guppylang/compiler/expr_compiler.py b/guppylang/compiler/expr_compiler.py index 28245824..999ca046 100644 --- a/guppylang/compiler/expr_compiler.py +++ b/guppylang/compiler/expr_compiler.py @@ -389,7 +389,7 @@ def python_value_to_hugr(v: Any, exp_ty: Type) -> ops.Value | None: assert isinstance(exp_ty, TupleType) vs = [ python_value_to_hugr(elt, ty) - for elt, ty in zip(elts, exp_ty.element_types) + for elt, ty in zip(elts, exp_ty.element_types, strict=True) ] if doesnt_contain_none(vs): return ops.Value(ops.TupleValue(vs=vs)) diff --git a/guppylang/decorator.py b/guppylang/decorator.py index 994b224a..2b5f94f5 100644 --- a/guppylang/decorator.py +++ b/guppylang/decorator.py @@ -1,3 +1,4 @@ +import ast import inspect from collections.abc import Callable from dataclasses import dataclass, field @@ -7,7 +8,7 @@ from hugr.serialization import ops, tys -from guppylang.ast_util import has_empty_body +from guppylang.ast_util import annotate_location, has_empty_body from guppylang.definition.common import DefId from guppylang.definition.custom import ( CustomCallChecker, @@ -18,6 +19,7 @@ RawCustomFunctionDef, ) from guppylang.definition.declaration import RawFunctionDecl +from guppylang.definition.extern import RawExternDef from guppylang.definition.function import RawFunctionDef, parse_py_func from guppylang.definition.parameter import TypeVarDef from guppylang.definition.struct import RawStructDef @@ -226,6 +228,43 @@ def dec(f: Callable[..., Any]) -> RawFunctionDecl: return dec + def extern( + self, + module: GuppyModule, + name: str, + ty: str, + symbol: str | None = None, + constant: bool = True, + ) -> RawExternDef: + """Adds an extern symbol to a module.""" + try: + type_ast = ast.parse(ty, mode="eval").body + except SyntaxError: + err = f"Not a valid Guppy type: `{ty}`" + raise GuppyError(err) from None + + # Try to annotate the type AST with source information. This requires us to + # inspect the stack frame of the caller + if frame := inspect.currentframe(): # noqa: SIM102 + if caller_frame := frame.f_back: # noqa: SIM102 + if caller_module := inspect.getmodule(caller_frame): + info = inspect.getframeinfo(caller_frame) + source_lines, _ = inspect.getsourcelines(caller_module) + source = "".join(source_lines) + annotate_location(type_ast, source, info.filename, 0) + # Modify the AST so that all sub-nodes span the entire line. We + # can't give a better location since we don't know the column + # offset of the `ty` argument + for node in [type_ast, *ast.walk(type_ast)]: + node.lineno, node.col_offset = info.lineno, 0 + node.end_col_offset = len(source_lines[info.lineno - 1]) + + defn = RawExternDef( + DefId.fresh(module), name, None, symbol or name, constant, type_ast + ) + module.register_def(defn) + return defn + def load(self, m: ModuleType | GuppyModule) -> None: caller = self._get_python_caller() if caller not in self._modules: diff --git a/guppylang/definition/declaration.py b/guppylang/definition/declaration.py index 13ccc06a..588599ef 100644 --- a/guppylang/definition/declaration.py +++ b/guppylang/definition/declaration.py @@ -66,7 +66,7 @@ def synthesize_call( return node, ty def compile_outer(self, graph: Hugr, parent: Node) -> "CompiledFunctionDecl": - """Adds a Hugr `FuncDecl` node for this funciton to the Hugr.""" + """Adds a Hugr `FuncDecl` node for this function to the Hugr.""" node = graph.add_declare(self.ty, parent, self.name) return CompiledFunctionDecl( self.id, self.name, self.defined_at, self.ty, self.python_func, node diff --git a/guppylang/definition/extern.py b/guppylang/definition/extern.py new file mode 100644 index 00000000..ba5d9e6b --- /dev/null +++ b/guppylang/definition/extern.py @@ -0,0 +1,77 @@ +import ast +from dataclasses import dataclass, field + +from hugr.serialization import ops + +from guppylang.ast_util import AstNode +from guppylang.checker.core import Globals +from guppylang.compiler.core import CompiledGlobals, DFContainer +from guppylang.definition.common import CompilableDef, ParsableDef +from guppylang.definition.value import CompiledValueDef, ValueDef +from guppylang.hugr_builder.hugr import Hugr, Node, OutPortV, VNode +from guppylang.tys.parsing import type_from_ast + + +@dataclass(frozen=True) +class RawExternDef(ParsableDef): + """A raw extern symbol definition provided by the user.""" + + symbol: str + constant: bool + type_ast: ast.expr + + description: str = field(default="extern", init=False) + + def parse(self, globals: Globals) -> "ExternDef": + """Parses and checks the user-provided signature of the function.""" + return ExternDef( + self.id, + self.name, + self.defined_at, + type_from_ast(self.type_ast, globals, None), + self.symbol, + self.constant, + self.type_ast, + ) + + +@dataclass(frozen=True) +class ExternDef(RawExternDef, ValueDef, CompilableDef): + """An extern symbol definition.""" + + def compile_outer(self, graph: Hugr, parent: Node) -> "CompiledExternDef": + """Adds a Hugr constant node for the extern definition to the provided graph.""" + custom_const = { + "symbol": self.symbol, + "typ": self.ty.to_hugr(), + "constant": self.constant, + } + value = ops.ExtensionValue( + extensions=["prelude"], + typ=self.ty.to_hugr(), + value=ops.CustomConst(c="ConstExternalSymbol", v=custom_const), + ) + const_node = graph.add_constant(ops.Value(value), self.ty, parent) + return CompiledExternDef( + self.id, + self.name, + self.defined_at, + self.ty, + self.symbol, + self.constant, + self.type_ast, + const_node, + ) + + +@dataclass(frozen=True) +class CompiledExternDef(ExternDef, CompiledValueDef): + """An extern symbol definition that has been compiled to a Hugr constant.""" + + const_node: VNode + + def load( + self, dfg: DFContainer, graph: Hugr, globals: CompiledGlobals, node: AstNode + ) -> OutPortV: + """Loads the extern value into a local Hugr dataflow graph.""" + return graph.add_load_constant(self.const_node.out_port(0)).out_port(0) diff --git a/guppylang/definition/struct.py b/guppylang/definition/struct.py index 508bc212..0d5a722a 100644 --- a/guppylang/definition/struct.py +++ b/guppylang/definition/struct.py @@ -116,8 +116,8 @@ def parse(self, globals: Globals) -> "ParsedStructDef": raise GuppyError("Unexpected statement in struct", node) # Ensure that functions don't override struct fields - if overriden := used_field_names.intersection(used_func_names.keys()): - x = overriden.pop() + if overridden := used_field_names.intersection(used_func_names.keys()): + x = overridden.pop() raise GuppyError( f"Struct `{self.name}` already contains a field named `{x}`", used_func_names[x], diff --git a/guppylang/error.py b/guppylang/error.py index 7bb1b455..17348d07 100644 --- a/guppylang/error.py +++ b/guppylang/error.py @@ -116,7 +116,9 @@ def format_source_location( ] longest = max(len(ln) for ln in line_numbers) prefixes = [ln + " " * (longest - len(ln) + indent) for ln in line_numbers] - res = "".join(prefix + line + "\n" for prefix, line in zip(prefixes, s[:-1])) + res = "".join( + prefix + line + "\n" for prefix, line in zip(prefixes, s[:-1], strict=False) + ) res += (longest + indent) * " " + s[-1] return res diff --git a/guppylang/hugr_builder/hugr.py b/guppylang/hugr_builder/hugr.py index 88ba20ae..55b25e2f 100644 --- a/guppylang/hugr_builder/hugr.py +++ b/guppylang/hugr_builder/hugr.py @@ -518,7 +518,7 @@ def add_tag( parent: Node | None = None, ) -> VNode: """Adds a `Tag` node to the graph.""" - assert all(inp.ty == ty for inp, ty in zip(inputs, variants[tag])) + assert all(inp.ty == ty for inp, ty in zip(inputs, variants[tag], strict=True)) hugr_variants = rows_to_hugr(variants) out_ty = SumType([row_to_type(row) for row in variants]) return self.add_node( diff --git a/guppylang/module.py b/guppylang/module.py index 2e4025ef..513bd658 100644 --- a/guppylang/module.py +++ b/guppylang/module.py @@ -36,6 +36,10 @@ class GuppyModule: # Whether the module has already been compiled _compiled: bool + # If the hugr has already been compiled, keeps a reference that can be returned + # from `compile`. + _compiled_hugr: Hugr | None + # Map of raw definitions in this module _raw_defs: dict[DefId, RawDef] _raw_type_defs: dict[DefId, RawDef] @@ -61,6 +65,7 @@ def __init__(self, name: str, import_builtins: bool = True): self._imported_globals = Globals.default() self._imported_compiled_globals = {} self._compiled = False + self._compiled_hugr = None self._instance_func_buffer = None self._raw_defs = {} self._raw_type_defs = {} @@ -162,10 +167,11 @@ def _check_defs( } @pretty_errors - def compile(self) -> Hugr | None: + def compile(self) -> Hugr: """Compiles the module and returns the final Hugr.""" if self.compiled: - raise GuppyError("Module has already been compiled") + assert self._compiled_hugr is not None, "Module is compiled but has no Hugr" + return self._compiled_hugr # Type definitions need to be checked first so that we can use them when parsing # function signatures etc. @@ -201,6 +207,7 @@ def compile(self) -> Hugr | None: defn.compile_inner(graph, all_compiled_globals) self._compiled = True + self._compiled_hugr = graph return graph def contains(self, name: str) -> bool: @@ -237,7 +244,7 @@ def get_py_scope(f: PyFunc) -> PyScope: nonlocals: PyScope = {} if f.__closure__ is not None: - for var, cell in zip(code.co_freevars, f.__closure__): + for var, cell in zip(code.co_freevars, f.__closure__, strict=True): try: value = cell.cell_contents except ValueError: diff --git a/guppylang/prelude/_internal.py b/guppylang/prelude/_internal.py index 8325faa6..fc62385c 100644 --- a/guppylang/prelude/_internal.py +++ b/guppylang/prelude/_internal.py @@ -48,9 +48,8 @@ class ConstF64(BaseModel): def bool_value(b: bool) -> ops.Value: """Returns the Hugr representation of a boolean value.""" - unit = ops.Value(ops.TupleValue(vs=[])) return ops.Value( - ops.SumValue(tag=int(b), typ=tys.SumType(tys.UnitSum(size=2)), vs=[unit]) + ops.SumValue(tag=int(b), typ=tys.SumType(tys.UnitSum(size=2)), vs=[]) ) diff --git a/guppylang/prelude/builtins.py b/guppylang/prelude/builtins.py index 2ab5e241..65a5abb0 100644 --- a/guppylang/prelude/builtins.py +++ b/guppylang/prelude/builtins.py @@ -2,10 +2,13 @@ # mypy: disable-error-code="empty-body, misc, override, valid-type, no-untyped-def" +from typing import Any + from hugr.serialization import tys from guppylang.decorator import guppy from guppylang.definition.custom import DefaultCallChecker, NoopCompiler +from guppylang.error import GuppyError from guppylang.hugr_builder.hugr import DummyOp from guppylang.module import GuppyModule from guppylang.prelude._internal import ( @@ -39,6 +42,15 @@ L = guppy.type_var(builtins, "L", linear=True) +def py(*_args: Any) -> Any: + """Function to tag compile-time evaluated Python expressions in a Guppy context. + + This function throws an error when execute in a Python context. It is only intended + to be used inside Guppy functions. + """ + raise GuppyError("`py` can only by used in a Guppy context") + + @guppy.extend_type(builtins, bool_type_def) class Bool: @guppy.custom(builtins, NoopCompiler()) diff --git a/guppylang/tys/var.py b/guppylang/tys/var.py index 43555d6f..fc332304 100644 --- a/guppylang/tys/var.py +++ b/guppylang/tys/var.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from typing import ClassVar -# Type of de Bruijn indicies +# Type of de Bruijn indices DeBruijn = int # Type of unique variable identifiers diff --git a/poetry.lock b/poetry.lock index 1e12d6ef..457bbf05 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1011,6 +1011,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1402,13 +1403,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] diff --git a/pyproject.toml b/pyproject.toml index 6046f1b6..2f3903b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "guppylang" -version = "0.3.0" +version = "0.5.2" description = "Pythonic quantum-classical programming language" authors = ["Mark Koch "] license = "Apache-2.0" diff --git a/release-please-config.json b/release-please-config.json index eb91fc5a..35868750 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -10,4 +10,4 @@ } }, "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" -} \ No newline at end of file +} diff --git a/ruff.toml b/ruff.toml index 0d1ffad9..b0a2c67b 100644 --- a/ruff.toml +++ b/ruff.toml @@ -71,7 +71,6 @@ ignore = [ "EM102", # Exception must not use a string (an f-string) literal, assign to variable first "S101", # Use of `assert` detected "TRY003", # Avoid specifying long messages outside the exception class - "B905", # `zip()` without an explicit `strict=` parameter ] [lint.per-file-ignores] diff --git a/tests/error/misc_errors/extern_bad_type.err b/tests/error/misc_errors/extern_bad_type.err new file mode 100644 index 00000000..382e00ea --- /dev/null +++ b/tests/error/misc_errors/extern_bad_type.err @@ -0,0 +1,7 @@ +Guppy compilation failed. Error in file $FILE:9 + +7: module = GuppyModule("test") +8: +9: guppy.extern(module, "x", ty="float[int]") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +GuppyError: Type `float` is not parameterized diff --git a/tests/error/misc_errors/extern_bad_type.py b/tests/error/misc_errors/extern_bad_type.py new file mode 100644 index 00000000..4f8e044d --- /dev/null +++ b/tests/error/misc_errors/extern_bad_type.py @@ -0,0 +1,12 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule +from guppylang.prelude.quantum import qubit + +import guppylang.prelude.quantum as quantum + + +module = GuppyModule("test") + +guppy.extern(module, "x", ty="float[int]") + +module.compile() diff --git a/tests/error/test_misc_errors.py b/tests/error/test_misc_errors.py index 03803e65..a82593c3 100644 --- a/tests/error/test_misc_errors.py +++ b/tests/error/test_misc_errors.py @@ -1,6 +1,8 @@ import pathlib import pytest +from guppylang import GuppyModule, guppy +from guppylang.error import GuppyError from tests.error.util import run_error_test path = pathlib.Path(__file__).parent.resolve() / "misc_errors" @@ -19,5 +21,12 @@ @pytest.mark.parametrize("file", files) -def test_type_errors(file, capsys): +def test_misc_errors(file, capsys): run_error_test(file, capsys) + + +def test_extern_bad_type_syntax(): + module = GuppyModule("test") + + with pytest.raises(GuppyError, match="Not a valid Guppy type: `foo bar`"): + guppy.extern(module, name="x", ty="foo bar") diff --git a/tests/integration/test_basic.py b/tests/integration/test_basic.py index d69f54e6..c3566b90 100644 --- a/tests/integration/test_basic.py +++ b/tests/integration/test_basic.py @@ -81,7 +81,21 @@ def test_func_decl_name(): def func_name() -> None: ... [def_op] = [ - n.op.root for n in module.compile().nodes() + n.op.root + for n in module.compile().nodes() if isinstance(n.op.root, ops.FuncDecl) ] assert def_op.name == "func_name" + + +def test_compile_again(): + module = GuppyModule("test") + + @guppy(module) + def identity(x: int) -> int: + return x + + hugr = module.compile() + + # Compiling again should return the same Hugr + assert hugr is module.compile() diff --git a/tests/integration/test_extern.py b/tests/integration/test_extern.py new file mode 100644 index 00000000..cfe2ff3f --- /dev/null +++ b/tests/integration/test_extern.py @@ -0,0 +1,51 @@ +from hugr.serialization import ops + +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + + +def test_extern_float(validate): + module = GuppyModule("module") + + guppy.extern(module, "ext", ty="float") + + @guppy(module) + def main() -> float: + return ext + ext # noqa: F821 + + hg = module.compile() + validate(hg) + + [c] = [n.op.root for n in hg.nodes() if isinstance(n.op.root, ops.Const)] + assert isinstance(c.v.root, ops.ExtensionValue) + assert c.v.root.value.v["symbol"] == "ext" + + +def test_extern_alt_symbol(validate): + module = GuppyModule("module") + + guppy.extern(module, "ext", ty="int", symbol="foo") + + @guppy(module) + def main() -> int: + return ext # noqa: F821 + + hg = module.compile() + validate(hg) + + [c] = [n.op.root for n in hg.nodes() if isinstance(n.op.root, ops.Const)] + assert isinstance(c.v.root, ops.ExtensionValue) + assert c.v.root.value.v["symbol"] == "foo" + + +def test_extern_tuple(validate): + module = GuppyModule("module") + + guppy.extern(module, "ext", ty="tuple[int, float]") + + @guppy(module) + def main() -> float: + x, y = ext # noqa: F821 + return x + y + + validate(module.compile()) diff --git a/tests/integration/test_py.py b/tests/integration/test_py.py index ce6accf9..3baa7592 100644 --- a/tests/integration/test_py.py +++ b/tests/integration/test_py.py @@ -4,8 +4,8 @@ from guppylang.decorator import guppy from guppylang.module import GuppyModule +from guppylang.prelude.builtins import py from guppylang.prelude.quantum import qubit, quantum -from tests.integration.util import py from tests.util import compile_guppy tket2_installed = find_spec("tket2") is not None diff --git a/tests/integration/test_struct.py b/tests/integration/test_struct.py index a37c12bc..a071ca6f 100644 --- a/tests/integration/test_struct.py +++ b/tests/integration/test_struct.py @@ -27,7 +27,9 @@ class DocstringStruct: x: int @guppy(module) - def main(a: EmptyStruct, b: OneMemberStruct, c: TwoMemberStruct, d: DocstringStruct) -> None: + def main( + a: EmptyStruct, b: OneMemberStruct, c: TwoMemberStruct, d: DocstringStruct + ) -> None: pass validate(module.compile()) @@ -94,4 +96,3 @@ def main(a: StructA[StructA[float]], b: StructB[int, bool], c: StructC) -> None: pass validate(module.compile()) - diff --git a/tests/integration/util.py b/tests/integration/util.py index 40232e58..fa3a7d4a 100644 --- a/tests/integration/util.py +++ b/tests/integration/util.py @@ -1,6 +1,3 @@ -from typing import Any - - class Decorator: def __matmul__(self, other): return None @@ -9,8 +6,3 @@ def __matmul__(self, other): # Dummy names to import to avoid errors for `_@functional` pseudo-decorator: functional = Decorator() _ = Decorator() - - -def py(*args: Any) -> Any: - """Dummy function to import to avoid errors for `py(...)` expressions""" - raise NotImplementedError diff --git a/validator/src/lib.rs b/validator/src/lib.rs index a730c6a1..cfe30be8 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -2,9 +2,9 @@ use hugr::extension::{ExtensionRegistry, PRELUDE}; use hugr::std_extensions::arithmetic::{float_ops, float_types, int_ops, int_types}; use hugr::std_extensions::collections; use hugr::std_extensions::logic; -use tket2::extension::{TKET1_EXTENSION, TKET2_EXTENSION}; use lazy_static::lazy_static; use pyo3::prelude::*; +use tket2::extension::{TKET1_EXTENSION, TKET2_EXTENSION}; lazy_static! { pub static ref REGISTRY: ExtensionRegistry = ExtensionRegistry::try_new([