Skip to content

feat!: Follow pass scopes in composable passes#1429

Merged
aborgna-q merged 10 commits intomainfrom
ab/pass-scopes
Mar 23, 2026
Merged

feat!: Follow pass scopes in composable passes#1429
aborgna-q merged 10 commits intomainfrom
ab/pass-scopes

Conversation

@aborgna-q
Copy link
Copy Markdown
Collaborator

@aborgna-q aborgna-q commented Mar 2, 2026

Adds support for PassScopes in the pass definitions. See Quantinuum/hugr#2772.

Requires a hugr release with the PassScope definition and Quantinuum/hugr#2910.

Many of these also call other passes from hugr_passes. While we pass the scope config along, proper support requires Quantinuum/hugr#2836 and Quantinuum/hugr#2871.

This is a rust-only change, the python interface will follow up.

BREAKING CHANGE: Multiple unit-like pass structs must now be constructed using a ::default() call instead.
BREAKING CHANGE: QSystemPass is now a ComposablePass. Import the trait to call run.
BREAKING CHANGE: QSystemPass no longer implements Copy.
BREAKING CHANGE: Renamed tket_qsystem::extension::qsystem::lower_tket_op to lower_tket_ops.

@hugrbot
Copy link
Copy Markdown
Collaborator

hugrbot commented Mar 2, 2026

This PR contains breaking changes to the public Rust API.

cargo-semver-checks summary
    Building tket v0.17.0 (current)
     Built [  42.793s] (current)
   Parsing tket v0.17.0 (current)
    Parsed [   0.083s] (current)
  Building tket v0.17.0 (baseline)
     Built [  41.164s] (baseline)
   Parsing tket v0.17.0 (baseline)
    Parsed [   0.078s] (baseline)
  Checking tket v0.17.0 -> v0.17.0 (assume minor change)
   Checked [   0.107s] 196 checks: 193 pass, 3 fail, 0 warn, 56 skip

--- failure derive_trait_impl_removed: built-in derived trait no longer implemented ---

Description:
A public type has stopped deriving one or more traits. This can break downstream code that depends on those types implementing those traits.
      ref: https://doc.rust-lang.org/reference/attributes/derive.html#derive
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.47.0/src/lints/derive_trait_impl_removed.ron

Failed in:
type NormalizeGuppy no longer derives Copy, in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/passes/guppy.rs:23
type NormalizeGuppy no longer derives Copy, in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/passes/guppy.rs:23

--- failure inherent_method_missing: pub method removed or renamed ---

Description:
A publicly-visible method or associated fn is no longer available under its prior name. It may have been renamed or removed entirely.
      ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.47.0/src/lints/inherent_method_missing.ron

Failed in:
BorrowSquashPass::set_regions, previously in file /home/runner/work/tket2/tket2/BASELINE_BRANCH/tket/src/passes/borrow_squash.rs:35
BorrowSquashPass::set_regions, previously in file /home/runner/work/tket2/tket2/BASELINE_BRANCH/tket/src/passes/borrow_squash.rs:35

--- failure unit_struct_changed_kind: unit struct changed kind ---

Description:
A public unit struct has been changed to a normal (curly-braces) struct, which cannot be constructed using the same struct literal syntax.
      ref: https://github.com/rust-lang/cargo/pull/10871
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.47.0/src/lints/unit_struct_changed_kind.ron

Failed in:
struct ModifierResolverPass in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/modifier/pass.rs:13

   Summary semver requires new major version: 3 major and 0 minor checks failed
  Finished [  86.544s] tket
  Building tket-qsystem v0.23.0 (current)
     Built [  41.636s] (current)
   Parsing tket-qsystem v0.23.0 (current)
    Parsed [   0.027s] (current)
  Building tket-qsystem v0.23.0 (baseline)
     Built [  41.833s] (baseline)
   Parsing tket-qsystem v0.23.0 (baseline)
    Parsed [   0.027s] (baseline)
  Checking tket-qsystem v0.23.0 -> v0.23.0 (assume minor change)
   Checked [   0.053s] 196 checks: 193 pass, 3 fail, 0 warn, 56 skip

--- failure derive_trait_impl_removed: built-in derived trait no longer implemented ---

Description:
A public type has stopped deriving one or more traits. This can break downstream code that depends on those types implementing those traits.
      ref: https://doc.rust-lang.org/reference/attributes/derive.html#derive
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.47.0/src/lints/derive_trait_impl_removed.ron

Failed in:
type QSystemPass no longer derives Copy, in /home/runner/work/tket2/tket2/PR_BRANCH/tket-qsystem/src/lib.rs:39

--- failure function_parameter_count_changed: pub fn parameter count changed ---

Description:
A publicly-visible function now takes a different number of parameters.
      ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.47.0/src/lints/function_parameter_count_changed.ron

Failed in:
tket_qsystem::extension::qsystem::check_lowered now takes 2 parameters instead of 1, in /home/runner/work/tket2/tket2/PR_BRANCH/tket-qsystem/src/extension/qsystem/lower.rs:283
tket_qsystem::extension::qsystem::lower_tk2_op now takes 2 parameters instead of 1, in /home/runner/work/tket2/tket2/PR_BRANCH/tket-qsystem/src/extension/qsystem/lower.rs:106

--- failure unit_struct_changed_kind: unit struct changed kind ---

Description:
A public unit struct has been changed to a normal (curly-braces) struct, which cannot be constructed using the same struct literal syntax.
      ref: https://github.com/rust-lang/cargo/pull/10871
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.47.0/src/lints/unit_struct_changed_kind.ron

Failed in:
struct LowerTketToQSystemPass in /home/runner/work/tket2/tket2/PR_BRANCH/tket-qsystem/src/extension/qsystem/lower.rs:322
struct ReplaceBoolPass in /home/runner/work/tket2/tket2/PR_BRANCH/tket-qsystem/src/replace_bools.rs:70
struct LowerDropsPass in /home/runner/work/tket2/tket2/PR_BRANCH/tket-qsystem/src/lower_drops.rs:15

   Summary semver requires new major version: 3 major and 0 minor checks failed
  Finished [  86.061s] tket-qsystem

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 2, 2026

Codecov Report

❌ Patch coverage is 68.13187% with 58 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.98%. Comparing base (f43a877) to head (f55c27f).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
tket-qsystem/src/lib.rs 58.53% 26 Missing and 8 partials ⚠️
tket-qsystem/src/extension/qsystem/lower.rs 80.95% 3 Missing and 5 partials ⚠️
tket/src/passes/guppy.rs 62.50% 2 Missing and 4 partials ⚠️
tket/src/modifier/pass.rs 0.00% 5 Missing ⚠️
tket-qsystem/src/replace_bools.rs 70.00% 0 Missing and 3 partials ⚠️
tket-qsystem/src/extension/qsystem/barrier.rs 0.00% 1 Missing ⚠️
tket/src/modifier/modifier_resolver.rs 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1429      +/-   ##
==========================================
+ Coverage   79.56%   79.98%   +0.42%     
==========================================
  Files         155      155              
  Lines       20245    20204      -41     
  Branches    19254    19213      -41     
==========================================
+ Hits        16107    16160      +53     
+ Misses       3186     3090      -96     
- Partials      952      954       +2     
Flag Coverage Δ
python 93.00% <ø> (ø)
qis-compiler 91.66% <100.00%> (-2.09%) ⬇️
rust 79.31% <67.95%> (+0.44%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@aborgna-q aborgna-q requested a review from acl-cqc March 3, 2026 15:50
@aborgna-q aborgna-q added this to the tket-py v0.13.0 milestone Mar 4, 2026
@aborgna-q aborgna-q changed the base branch from main to ab/hugr-0.26.0 March 16, 2026 15:43
@aborgna-q aborgna-q force-pushed the ab/hugr-0.26.0 branch 10 times, most recently from cb4d32c to 2b29cdb Compare March 18, 2026 17:25
github-merge-queue bot pushed a commit that referenced this pull request Mar 19, 2026
Updates the hugr dependencies to `0.26.0`, and fixes all the breaking
changes.

I only added no-op stubs implementing `WithScope` for the local passes.
We will add the actual implementation in
#1429.

Requires updating LLVM too, so we should merge
#1422 into this PR. We also need
to delete the now removed `stack_array` lowering.

BREAKING CHANGE: Updated public `hugr` dependency to `0.26.0`.

Requires a `hugr-passes 0.26.1` patch release to include
Quantinuum/hugr#2954

---------

Co-authored-by: George Hodgkins <[email protected]>
Co-authored-by: Jake Arkinstall <[email protected]>
Base automatically changed from ab/hugr-0.26.0 to main March 19, 2026 10:33
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 diff is a bit noisy since I cleaned up the definitions, but the logic should be the same;
we are just passing the scope to the internal calls, and using scope.root instead of the entrypoint in force_order.

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.

With the scope updates we can now re-enable this test.

I commented out the GPU test since it just lowers to an empty program, and there is no guppy definition from where to re-generate the Hugr.

Review recommendation:

image

@aborgna-q aborgna-q marked this pull request as ready for review March 19, 2026 12:29
@aborgna-q aborgna-q requested a review from a team as a code owner March 19, 2026 12:29
Copy link
Copy Markdown
Contributor

@acl-cqc acl-cqc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aborgna-q this looks great - only one significant concern: https://github.com/Quantinuum/tket2/pull/1429/changes#r2974713146 which I think can be addressed by doing the (legacy!) "module-root support" much earlier, although have a think about whether we really wanna allow doing QSystemPass on an entrypoint scope.

Other than that a bunch of nits but nothing at all major :-)

///
/// Returns an error if the replacement fails.
pub fn lower_tk2_op(hugr: &mut impl HugrMut<Node = Node>) -> Result<Vec<Node>, LowerTk2Error> {
pub fn lower_tk2_op(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive-by super-nit: this really should be lower_tk2_ops, there are many of them, it does not take an argument saying which one to lower!

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.

Renamed it. This is a breaking change already, so not bothering with deprecation.

// Only perform multi-op replacement for global passes, as we
// cannot define new functions for local entrypoint scopes.
if let PassScope::Global(_) = scope {
let template = match funcs.entry(tket_op) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this funcs.entry(tket_op).get_or_insert_with(|| ...).clone()

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.

Almost, because build_func returns a Result.

self.force_order(hugr)?;
}

// Backwards compatibility: If the entrypoint is a module, find a function named "main" and set that as
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, so if we are running with PassScope::Entrypoint(Flat|Recursive) and the entrypoint is the module, then we've been doing nothing up to this point, and now we suddenly start running on just the main function...

I slightly question whether it even makes sense to run this on an entrypoint scope, i.e. whether this should even be a ComposablePass (or should just take a Preserve - to distinguish between library/executable modes); indeed whether it makes sense to run this on a library, or only post-linking. (Ok if we want to do LLVM linking then probably we do want to run this in library mode first).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, we say "expects the hugr to have a function entrypoint" - and if it doesn't, we'll error if we don't have a main. So (a) might want to mention this legacy support in the doc (or might not!); (b) we are committed not to be compiling a library, so definitely should use Preserve::Entrypoint (even if not in this PR), but def. should do this at the beginning I think.

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.

I do prefer having a standardized definition for passes. Perhaps we should have defined a "only global" pass type, but an alternative here could be just erroring out when called with a local scope (to avoid the user dealing with unexpected behaviour).

At this point I'm not sure what workflows are depending on the backwards compatibility for module entrypoints.
I didn't want to break things in this PR, but I agree that we should be erroring out if the entrypoint is not a function.
That may need some extra discussion and testing though to make sure we don't break downstream. I'll open an issue to decide what we should do here, but merge this with the compat option still in.

use ModifierResolverErrors::*;

let entry_points: VecDeque<_> = entry_points.into_iter().collect();
let entry_points: Vec<_> = entry_points.into_iter().collect();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have you changed this VecDeque -> Vec ?

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.

It was a leftover from a previous change. Rolled it back to VecDeque.

while let Some(region) = regions.pop_front() {
let is_dataflow_region = OpTag::DataflowParent >= hugr.get_optype(region).tag();
seen.clear();
local_queue.clear();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: define local_queue here, then no need to clear it. (We end with a while let Some(...) = local_queue.pop() so it really should be empty anyway)

Maybe call it op_queue too

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.

It's a simple opt to avoid reallocating memory on every loop.
We can just reuse the container instead.

.flat_map(|n| all_outs(hugr, n)),
);
for child in hugr.children(region) {
// If the node is not reachable along dataflow edges from other nodes,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this just repeats the comment two lines before

@aborgna-q aborgna-q added this pull request to the merge queue Mar 23, 2026
Merged via the queue into main with commit ae0016c Mar 23, 2026
23 of 24 checks passed
@aborgna-q aborgna-q deleted the ab/pass-scopes branch March 23, 2026 16:30
github-merge-queue bot pushed a commit that referenced this pull request Apr 2, 2026
## 🤖 New release

* `tket`: 0.17.0 -> 0.18.0 (✓ API compatible changes)
* `tket-qsystem`: 0.23.0 -> 0.24.0 (✓ API compatible changes)

<details><summary><i><b>Changelog</b></i></summary><p>

## `tket`

<blockquote>

##
[0.18.0](tket-v0.17.0...tket-v0.18.0)
- 2026-04-02

### Bug Fixes

- *(pytket decoder)* Panic on repeated bit registers in pytket decoded
output ([#1445](#1445))
- pytket encoder drops order edges to the output node
([#1466](#1466))

### Documentation

- Fix tket README introductory example
([#1463](#1463))

### New Features

- [**breaking**] Use raw Hugrs in pytket encoding/decoding API
([#1418](#1418))
- [**breaking**] Remove unused `lower_to_pytket` pass
([#1431](#1431))
- [**breaking**] Replace CircuitHash with hugr's implementation
([#1420](#1420))
- [**breaking**] Update MSRV to rust 1.91
([#1446](#1446))
- [**breaking**] Update to hugr 0.26.0
([#1448](#1448))
- [**breaking**] Follow pass scopes in composable passes
([#1429](#1429))
- Implemented `post_opdef` for `RotationOp` for constant folding
([#1468](#1468))
- [**breaking**] Reorganize `tket::passes` and add `hugr_passes`
re-exports ([#1472](#1472))
- [**breaking**] Bump `hugr` dependency to 0.27.0
([#1488](#1488))
- Move hugr-passes implementations to tket::passes
([#1487](#1487))
- Pass scopes in python API, update to hugr-py 0.16
([#1464](#1464))

### Refactor

- *(llvm)* use llvm.is.fpclass for from_halfturns
([#1457](#1457))

### Testing

- Fixed signatures when decoding pytket circuits
([#1405](#1405))
</blockquote>

## `tket-qsystem`

<blockquote>

##
[0.24.0](tket-qsystem-v0.23.0...tket-qsystem-v0.24.0)
- 2026-04-02

### Bug Fixes

- pytket encoder drops order edges to the output node
([#1466](#1466))
- Constant Folding with PassScope::Global should act globally, not just
beneath the entrypoint
([#1470](#1470))

### New Features

- [**breaking**] Use raw Hugrs in pytket encoding/decoding API
([#1418](#1418))
- Add qsystem.rz pytket decoder
([#1432](#1432))
- [**breaking**] Update MSRV to rust 1.91
([#1446](#1446))
- [**breaking**] Update to hugr 0.26.0
([#1448](#1448))
- [**breaking**] Follow pass scopes in composable passes
([#1429](#1429))
- [**breaking**] Reorganize `tket::passes` and add `hugr_passes`
re-exports ([#1472](#1472))
- Move hugr-passes implementations to tket::passes
([#1487](#1487))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

---------

Co-authored-by: Agustín Borgna <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants