Skip to content

Conversation

tatiana-s
Copy link
Contributor

@tatiana-s tatiana-s commented Jul 17, 2025

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 dest functions 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)

@tatiana-s tatiana-s requested a review from a team as a code owner July 17, 2025 08:56
@tatiana-s tatiana-s requested a review from cqc-alec July 17, 2025 08:56
@tatiana-s tatiana-s marked this pull request as draft July 17, 2025 08:57
@tatiana-s tatiana-s removed the request for review from cqc-alec July 17, 2025 08:57
Copy link

codecov bot commented Jul 17, 2025

Codecov Report

❌ Patch coverage is 97.28850% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.29%. Comparing base (64412df) to head (5eed713).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
tket-qsystem/src/replace_borrow_arrays.rs 97.68% 15 Missing and 4 partials ⚠️
tket-qsystem/src/replace_bools.rs 94.87% 3 Missing and 1 partial ⚠️
tket-qsystem/src/lib.rs 71.42% 1 Missing and 1 partial ⚠️
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     
Flag Coverage Δ
rust 79.60% <97.28%> (+1.21%) ⬆️

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.

@tatiana-s tatiana-s marked this pull request as ready for review July 23, 2025 09:47
@tatiana-s tatiana-s requested a review from doug-q July 23, 2025 09:48
@mark-koch mark-koch self-requested a review July 23, 2025 10:04
@aborgna-q aborgna-q removed the request for review from doug-q July 23, 2025 10:04
Copy link
Contributor

@mark-koch mark-koch left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines 51 to 52
// TODO uncomment once https://github.com/CQCL/hugr/issues/1234 is complete
// ensure_no_nonlocal_edges(hugr)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems to be done?

Copy link
Contributor

Choose a reason for hiding this comment

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

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// 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)
Copy link
Contributor

Choose a reason for hiding this comment

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

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// 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).
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Unwrap the result: expect that the slot was empty (none).
// Unwrap the result: expect that the index was in bounds.

Comment on lines 475 to 476
// Unpack the array to get the elements as wires
let elems = dfb.add_array_unpack(elem_ty.clone(), size, arr).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes the replacement scale with the array size. Better to use a scan op to wrap the elements into options

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Contributor

Choose a reason for hiding this comment

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

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...

Comment on lines 527 to 530
// Unwrap all the options in the input array.
let elems = dfb
.add_array_unpack(opt_src_ty.clone().into(), size, inputs[0])
.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here: Better to unpack using a scan

},
);

lw
Copy link
Contributor

Choose a reason for hiding this comment

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

You're missing the conversion ops from and to regular arrays

Comment on lines 602 to 610
move |args| {
let [size, elem_ty] = args else {
unreachable!()
};
Some(borrow_dest(
size.as_nat().unwrap(),
elem_ty.as_runtime().unwrap(),
))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you also add a test for array constants?

@tatiana-s tatiana-s requested a review from mark-koch July 25, 2025 12:00
@tatiana-s tatiana-s requested a review from doug-q July 28, 2025 09:11
@ss2165 ss2165 mentioned this pull request Jul 28, 2025
Copy link
Contributor

@mark-koch mark-koch left a comment

Choose a reason for hiding this comment

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

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?

Copy link
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.

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?

vec![array_ty.clone(), array_ty],
))
.unwrap();
// TODO: Find less hacky solution.
Copy link
Contributor

Choose a reason for hiding this comment

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

Have we lowered drops already? (If not, can we lower to a drop?)

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))
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider (!elem_ty.copyable()).then(|| array_clone_dest(size,elem_ty)) instead of the if...else

@@ -18,6 +22,28 @@ impl<H: HugrMut<Node = Node>> ComposablePass<H> for LowerDropsPass {

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need this? If so, I stand by my previous comment; defining a private helper function is easy enough to do now

Comment on lines 51 to 52
// TODO uncomment once https://github.com/CQCL/hugr/issues/1234 is complete
// ensure_no_nonlocal_edges(hugr)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

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 !).

@mark-koch
Copy link
Contributor

@mark-koch what do you think?

Yeah imo ok to merge and make improvements in follow-up PRs

Copy link
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 Tatiana, mammoth chunk of work, Rust hugr-building is still painful and you've had to do a lot of it here - well done! 👍 💯 🥇

@@ -44,6 +46,7 @@ pub struct QSystemPass {
monomorphize: bool,
force_order: bool,
lazify: bool,
lower_borrow_arrays: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

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 ;-)

@@ -18,6 +22,28 @@ impl<H: HugrMut<Node = Node>> ComposablePass<H> for LowerDropsPass {

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

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()];
Copy link
Contributor

Choose a reason for hiding this comment

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

[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()));
Copy link
Contributor

Choose a reason for hiding this comment

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

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(),
Copy link
Contributor

Choose a reason for hiding this comment

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

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(),
Copy link
Contributor

Choose a reason for hiding this comment

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

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()],
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can skip vec! if singleton

@tatiana-s tatiana-s added this pull request to the merge queue Sep 4, 2025
Merged via the queue into main with commit ca5f19c Sep 4, 2025
20 checks passed
@tatiana-s tatiana-s deleted the ts/replace-ba branch September 4, 2025 10:12
@hugrbot hugrbot mentioned this pull request Sep 4, 2025
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.

Lowering for borrow_array
3 participants