feat: Add a borrow_array type replacement pass#975
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #975 +/- ##
==========================================
+ Coverage 79.18% 80.29% +1.10%
==========================================
Files 120 121 +1
Lines 14147 15069 +922
Branches 13365 14287 +922
==========================================
+ Hits 11202 12099 +897
- Misses 2269 2288 +19
- Partials 676 682 +6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
mark-koch
left a comment
There was a problem hiding this comment.
Thanks, must have been quite tedious to write all that 😅
Looks good to me, mostly just a few nits and refactoring suggestions apart from the get lowering comment and missing lowering for conversion ops.
| // TODO uncomment once https://github.com/CQCL/hugr/issues/1234 is complete | ||
| // ensure_no_nonlocal_edges(hugr)?; |
There was a problem hiding this comment.
Not quite sure what's going on here but we "temporarily" added a guppy fix to avoid generating nonlocals, however now Hugr has LocalizeEdges pass, so can we remove the guppy workaround? Not for this PR, but do we have a guppy issue?
Here, I'm not sure it's worth adding an error variant for the existence of nonlocal edges, to me I think this is an assert (a bug in our code) if we have not run LocalizeEdges already (or if there is a bug in LocalizeEdges !).
| // pass meant to be run on Guppy-generated HUGRs, which should not emit borrow array | ||
| // constants that contain empty values. The borrow array extension however does not | ||
| // guarantee this, so we should handle the case where the array contains empty | ||
| // values on we add a more specific borrow array value that supports this. |
There was a problem hiding this comment.
| // values on we add a more specific borrow array value that supports this. | |
| // values once we add a more specific borrow array value that supports this. |
?
| // Unwrap the result: expect that the element was present. | ||
| let result_ty = vec![opt_elem_ty.clone().into(), arr_type.clone()]; | ||
| let [opt_elem, out_arr] = dfb | ||
| .build_unwrap_sum(1, either_type(result_ty.clone(), result_ty), result) |
There was a problem hiding this comment.
Would be nicer to have an unwrap with a custom panic message. E.g. in Guppy we say "Index out of bounds"
| .add_array_set(opt_elem_ty.clone().into(), size, arr, idx, nothing) | ||
| .unwrap(); | ||
|
|
||
| // Unwrap the result: expect that the element was present. |
There was a problem hiding this comment.
| // Unwrap the result: expect that the element was present. | |
| // Unwrap the result: expect that the index was in bounds. |
| .add_array_set(opt_elem_ty.clone().into(), size, arr, idx, opt_elem) | ||
| .unwrap(); | ||
|
|
||
| // Unwrap the result: expect that the slot was empty (none). |
There was a problem hiding this comment.
| // Unwrap the result: expect that the slot was empty (none). | |
| // Unwrap the result: expect that the index was in bounds. |
| // Unpack the array to get the elements as wires | ||
| let elems = dfb.add_array_unpack(elem_ty.clone(), size, arr).unwrap(); |
There was a problem hiding this comment.
This makes the replacement scale with the array size. Better to use a scan op to wrap the elements into options
There was a problem hiding this comment.
I had issues using scan because when I try to define a function on the module builder I get from the DFG builder it doesn't seem to add the function so I can't load it in to use with scan
There was a problem hiding this comment.
Mhmm that's not ideal, pinging @acl-cqc
I guess it's ok to play around with for now but I'm sceptical about using this in production since it makes compile-time and binary size scale with arrays sizes...
| // Unwrap all the options in the input array. | ||
| let elems = dfb | ||
| .add_array_unpack(opt_src_ty.clone().into(), size, inputs[0]) | ||
| .unwrap(); |
There was a problem hiding this comment.
Same here: Better to unpack using a scan
| }, | ||
| ); | ||
|
|
||
| lw |
There was a problem hiding this comment.
You're missing the conversion ops from and to regular arrays
| move |args| { | ||
| let [size, elem_ty] = args else { | ||
| unreachable!() | ||
| }; | ||
| Some(borrow_dest( | ||
| size.as_nat().unwrap(), | ||
| elem_ty.as_runtime().unwrap(), | ||
| )) | ||
| } |
There was a problem hiding this comment.
There's a lot of duplication of this logic. You could consider commoning it up a bit:
for op_def in BArrayUnsafeOpDef::iter() {
w.replace_parametrized_op(
borrow_array::EXTENSION.get_op(&op_def.opdef_id()).unwrap(),
move |args| {
let [size, elem_ty] = args else {
unreachable!()
};
let size = size.as_nat().unwrap();
let elem_ty = elem_ty.as_runtime().unwrap();
Some(match op_def {
BArrayUnsafeOpDef::borrow => borrow_dest(size, elem_ty),
BArrayUnsafeOpDef::#return => return_dest(size, elem_ty),
...
})
},
);
}| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { |
There was a problem hiding this comment.
Could you also add a test for array constants?
mark-koch
left a comment
There was a problem hiding this comment.
Thanks! Looks good, happy to merge this
Can you create an issue to use scan instead of array unpacking and link it in the relevant places?
acl-cqc
left a comment
There was a problem hiding this comment.
TBH I feel that if this is passing tests then rather than benchmarking performance first, we should just get this in; and then work on adding benchmarks and improving/optimizing later. (e.g. from serialized Hugrs, so we are not waiting for guppy-main to generate them; and measuring compilation time and size of compiled output, down to whatever level is easy to test - maybe the Hugr input to hugr-llvm, or the LLVM-IR output from hugr-llvm, without running LLVM itself.) @mark-koch what do you think?
tket-qsystem/src/replace_bools.rs
Outdated
| vec![array_ty.clone(), array_ty], | ||
| )) | ||
| .unwrap(); | ||
| // TODO: Find less hacky solution. |
There was a problem hiding this comment.
Have we lowered drops already? (If not, can we lower to a drop?)
tket-qsystem/src/replace_bools.rs
Outdated
| let size = size.as_nat().unwrap(); | ||
| let elem_ty = elem_ty.as_runtime().unwrap(); | ||
| if !elem_ty.copyable() { | ||
| Some(array_clone_dest(size, elem_ty)) |
There was a problem hiding this comment.
Consider (!elem_ty.copyable()).then(|| array_clone_dest(size,elem_ty)) instead of the if...else
| fn run(&self, hugr: &mut H) -> Result<Self::Result, Self::Error> { | ||
| let mut rt = ReplaceTypes::default(); | ||
|
|
||
| // future(bool) is not in the default linearizer handler so we add it here. |
There was a problem hiding this comment.
I think I favour commoning this up with the equivalent code in replace_bools.rs by making a helper-method that makes a ReplaceTypes with future-bool linearizer already set-up.
There was a problem hiding this comment.
Do we still need this? If so, I stand by my previous comment; defining a private helper function is easy enough to do now
| // TODO uncomment once https://github.com/CQCL/hugr/issues/1234 is complete | ||
| // ensure_no_nonlocal_edges(hugr)?; |
There was a problem hiding this comment.
Not quite sure what's going on here but we "temporarily" added a guppy fix to avoid generating nonlocals, however now Hugr has LocalizeEdges pass, so can we remove the guppy workaround? Not for this PR, but do we have a guppy issue?
Here, I'm not sure it's worth adding an error variant for the existence of nonlocal edges, to me I think this is an assert (a bug in our code) if we have not run LocalizeEdges already (or if there is a bug in LocalizeEdges !).
Yeah imo ok to merge and make improvements in follow-up PRs |
acl-cqc
left a comment
There was a problem hiding this comment.
Thanks Tatiana, mammoth chunk of work, Rust hugr-building is still painful and you've had to do a lot of it here - well done! 👍 💯 🥇
| monomorphize: bool, | ||
| force_order: bool, | ||
| lazify: bool, | ||
| lower_borrow_arrays: bool, |
There was a problem hiding this comment.
I could super-nit-pick and say that this should be replace_borrow_arrays....or all should be called borrow_array_to_array.....but I won't ;-)
| fn run(&self, hugr: &mut H) -> Result<Self::Result, Self::Error> { | ||
| let mut rt = ReplaceTypes::default(); | ||
|
|
||
| // future(bool) is not in the default linearizer handler so we add it here. |
There was a problem hiding this comment.
Do we still need this? If so, I stand by my previous comment; defining a private helper function is easy enough to do now
| .unwrap(); | ||
|
|
||
| // Unwrap the result: expect that the index was in bounds. | ||
| let result_ty = vec![opt_elem_ty.clone().into(), arr_type.clone()]; |
There was a problem hiding this comment.
[Optional!] Could common up this bounds-check build_expect with the one in return_dest (helper method needs take only dfb, arr_type as the element type can be extracted from that, and result)
|
|
||
| // Check all elements are none and then discard array with now droppable elements. | ||
| let scan_op = ArrayScan::new(opt_elem_ty.clone().into(), Type::UNIT, vec![], size); | ||
| let unit_map_func = dfb.add_load_value(unit_map_func_value(elem_ty.clone())); |
There was a problem hiding this comment.
This isn't too bad if one forgets about needing unit_map_func_value 😁 ...ok good stuff 👍
| Tag::new( | ||
| 0, | ||
| vec![ | ||
| vec![elem_ty.clone(), arr_type.clone()].into(), |
There was a problem hiding this comment.
Getting a bit hard to read - do let tys = vec![elem_ty.clone(), arr_type.clone()].into(); earlier and then here you can just vec![tys.clone(), tys] here
| Tag::new( | ||
| 1, | ||
| vec![ | ||
| vec![elem_ty.clone(), arr_type.clone()].into(), |
There was a problem hiding this comment.
and again using the same tys as above
| fn option_wrap_func_value(elem_ty: Type) -> Value { | ||
| let func_hugr = { | ||
| let mut dfb = DFGBuilder::new(inout_sig( | ||
| vec![elem_ty.clone()], |
There was a problem hiding this comment.
nit: can skip vec! if singleton
## 🤖 New release * `tket`: 0.14.0 -> 0.15.0 (✓ API compatible changes) * `tket-qsystem`: 0.20.1 -> 0.21.0 (✓ API compatible changes) <details><summary><i><b>Changelog</b></i></summary><p> ## `tket` <blockquote> ## [0.15.0](tket-v0.14.0...tket-v0.15.0) - 2025-09-15 ### Bug Fixes - [**breaking**] Fix rotation -> float param type conversion ([#1061](#1061)) - Pytket barrier operations not being decoded ([#1069](#1069)) - Always load parameter expressions as half turns in the decoder ([#1083](#1083)) - Move attribute to come after all the cases. ([#1112](#1112)) ### New Features - Capture pytket's output permutation explicitly in the hugr connectivity ([#1075](#1075)) - Add ResourceScope ([#1052](#1052)) - [**breaking**] Remove unnecessary Arc from PytketDecoder method ([#1114](#1114)) - [**breaking**] Remove deprecated definitions ([#1113](#1113)) </blockquote> ## `tket-qsystem` <blockquote> ## [0.21.0](tket-qsystem-v0.20.1...tket-qsystem-v0.21.0) - 2025-09-15 ### Bug Fixes - [**breaking**] Fix rotation -> float param type conversion ([#1061](#1061)) - Pytket barrier operations not being decoded ([#1069](#1069)) - *(qystem)* fix angle bug in CZ decomposition ([#1080](#1080)) - Always load parameter expressions as half turns in the decoder ([#1083](#1083)) ### New Features - Add a `borrow_array` type replacement pass ([#975](#975)) - Add gpu module ([#1090](#1090)) - [**breaking**] Remove unnecessary Arc from PytketDecoder method ([#1114](#1114)) - [**breaking**] Remove deprecated definitions ([#1113](#1113)) ### Refactor - [**breaking**] Factor out wasm extension code into compute module ([#1089](#1089)) </blockquote> </p></details> --- This PR was generated with [release-plz](https://github.com/release-plz/release-plz/). --------- Co-authored-by: Agustín Borgna <121866228+aborgna-q@users.noreply.github.com> Co-authored-by: Alec Edgington <54802828+cqc-alec@users.noreply.github.com>
…#1166) Closes #1072 BREAKING CHANGE: (guppy-internals) Arrays are now lowered to `borrow_array`s instead of `value_array`s so elements do no longer need to be wrapped in options during lowering. ~~Requires a tket2 release with Quantinuum/tket2#975 for the execution tests to pass~~ Requires a hugr-llvm release for the lowering + hugr with copy discard handler + tket2 with adjustments + hugr-qis compiler with adjustments --------- Co-authored-by: Seyon Sivarajah <seyon.sivarajah@quantinuum.com> Co-authored-by: Mark Koch <mark.koch@quantinuum.com> Co-authored-by: Alan Lawrence <alan.lawrence@cambridgequantum.com>
Draft because performance benchmarks with Guppy using this are needed first to see if this is suitable. There are probably a lot of ways that some of the more repetitive code in all the
destfunctions could be generalised but for now I prioritised getting something working.Closes #2397 (but should add a new issue for doing this properly in LLVM at some point)