Skip to content

Conversation

mark-koch
Copy link
Contributor

Closes #2551

The first two commits just copy the existing lowering. The changes to the lowering lgic are implemented in 888e457.

@mark-koch mark-koch requested a review from tatiana-s September 22, 2025 16:45
@mark-koch mark-koch requested a review from a team as a code owner September 22, 2025 16:45
@mark-koch mark-koch requested review from acl-cqc and removed request for a team and acl-cqc September 22, 2025 16:45
Copy link

codecov bot commented Sep 22, 2025

Codecov Report

❌ Patch coverage is 91.66667% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.27%. Comparing base (69a126d) to head (a616a8e).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
hugr-llvm/src/emit/func.rs 91.48% 1 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2574      +/-   ##
==========================================
+ Coverage   83.07%   83.27%   +0.19%     
==========================================
  Files         255      256       +1     
  Lines       48436    50444    +2008     
  Branches    43946    45967    +2021     
==========================================
+ Hits        40240    42005    +1765     
+ Misses       6103     6064      -39     
- Partials     2093     2375     +282     
Flag Coverage Δ
python 91.53% <ø> (-0.03%) ⬇️
rust 82.46% <91.66%> (+0.25%) ⬆️

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.

let func = ctx
.get_current_module()
.get_function(func_name)
.map(Ok::<_, anyhow::Error>)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Option of Result, etc., is a bit hard to figure out. How about let func = match ctx.get_current_module().get_function(func_name) { Some(f) => f, None => { .... } } which moves your closure into the body and removes the nested ?.

Or perhaps consider let func = if let Some(f) = ctx.blah() {f} else {....}

}

impl<'a, H: HugrView<Node = Node> + 'a> CodegenExtsBuilder<'a, H> {
/// Add a [`BorrowArrayCodegenExtension`] to the given [`CodegenExtsBuilder`] using `ccg`
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: comments should be the other way round?

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.

Well I thought I'd take a look at this, but +4,000 lines and I'm stopping a fraction of the way through.... the PR we are thinking this replaces is a third as long as this, right? How many opportunities are there for bugs here? (ReplaceTypes BorrowArray has the benefit that we get type-checking of the output Hugr by the hugr type system, too)

/// The first time this helper is called with a given function name, a function is built
/// using the provided closure. Future invocations with the same name will just emit calls
/// to this function.
pub fn outline_into_function<'c, H: HugrView<Node = Node>, const N: usize>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps get_or_make_function

//! to at least `n * sizeof(T)` bytes. The extra `usize` is an offset pointing to the
//! first element, i.e. the first element is at address `ptr + offset * sizeof(T)`.
//!
//! The rational behind the additional offset is the `pop_left` operation which bumps
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this explanation, but is there no push_left / unpop ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, rational -> rationale

Copy link
Contributor Author

Choose a reason for hiding this comment

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

is there no push_left / unpop ?

No, as this would require reallocating the array in general

//! the offset instead of mutating the pointer. This way, we can still free the original
//! pointer when the array is discarded after a pop.
//!
//! We provide utility functions [`array_fat_pointer_ty`], [`build_barray_fat_pointer`], and
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are some of these barray and others just array ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I forgot to rename those after copy pasting 😅

signal: 2,
message: "Array element is already borrowed".to_string(),
};
static ref ERR_NOT_FREE: ConstError = ConstError {
Copy link
Contributor

Choose a reason for hiding this comment

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

ERR_NOT_BORROWED ? Is this the dual of the previous?

}
}

/// A helper trait for customising the lowering of [`hugr_core::std_extensions::collections::array`]
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: ambiguous as to whether the array adjective refers just to types, or also CustomConsts and ops...perhaps customising the lowering of types, CustomConsts and ops, that are from array but if there's no good phrasing consider brackets ;)

/// A helper trait for customising the lowering of [`hugr_core::std_extensions::collections::array`]
/// types, [`hugr_core::ops::constant::CustomConst`]s, and ops.
///
/// An `array<n, T>` is now lowered to a fat pointer `{ptr, usize}` that is allocated
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicates the module doc...can we crosslink instead

Copy link
Contributor

Choose a reason for hiding this comment

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

Although, one of these ought to describe the lowering of a borrow array rather than an array, i.e. with a bitmask too??

Ok(array_v.into_struct_value())
}

/// Returns the underlying pointer and offset stored in a fat borrow array pointer.
Copy link
Contributor

Choose a reason for hiding this comment

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

pointer, mask and offset

.build_bit_cast(mask_ptr, usize_t.ptr_type(AddressSpace::default()), "")?
.into_pointer_value();
// Initialise mask using memset
let memset_intrinsic = Intrinsic::find("llvm.memset").unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: not sure the intrinsic is helpful here, just inline?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure what you mean? Why not use an intrinsics, it helps LLVM reason about the semantics of funtions?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I mean inline the variable memset_intrinsic - I am sure using llvm.memset is the right thing to do!

Copy link
Contributor

Choose a reason for hiding this comment

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

I've done this

} else {
ctx.iw_context().i8_type().const_zero()
};
let volatile = ctx.iw_context().bool_type().const_zero().into();
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this?....an argument to pass to memset, i.e. that the mask is not volatile?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tatiana-s
Copy link
Contributor

Well I thought I'd take a look at this, but +4,000 lines and I'm stopping a fraction of the way through.... the PR we are thinking this replaces is a third as long as this, right? How many opportunities are there for bugs here? (ReplaceTypes BorrowArray has the benefit that we get type-checking of the output Hugr by the hugr type system, too)

But most of this PR just copies the existing array lowering (including many snapshots) so it is only the bit mask code that is new which is a lot less than the type replacement

@mark-koch mark-koch force-pushed the feat/barray-llvm branch 4 times, most recently from d82bb30 to ba4df3d Compare September 29, 2025 14:05
Base automatically changed from test/panic to main September 29, 2025 15:46
@ss2165
Copy link
Member

ss2165 commented Oct 6, 2025

Please include #2609 before merging done

@acl-cqc
Copy link
Contributor

acl-cqc commented Oct 6, 2025

Apologies for force-push but I made sure the old/new commits were identical by git diff. The force-push has had the effect of removing, from the commit-list of this PR, both #2568 and all merges with main.

The first commit bdc4795 duplicates hugr-llvm/src/collections/array.rs to borrow_array.rs. After that, the remaining changes are "only":

.../std_extensions/collections/borrow_array.rs  |    2 +-
 hugr-llvm/src/emit/func.rs                      |   69 +-
 .../src/extension/collections/borrow_array.rs   | 1612 ++++++++++++++---
 ...borrow_array__test__emit_all_ops@llvm14.snap |  Bin 0 -> 16356 bytes
 ...__test__emit_all_ops@pre-mem2reg@llvm14.snap |  Bin 0 -> 27469 bytes
 ...ow_array__test__emit_array_value@llvm14.snap |  Bin 0 -> 1303 bytes
 ...st__emit_array_value@pre-mem2reg@llvm14.snap |  Bin 0 -> 1685 bytes
 ...__borrow_array__test__emit_clone@llvm14.snap |  Bin 0 -> 4753 bytes
 ...ay__test__emit_clone@pre-mem2reg@llvm14.snap |  Bin 0 -> 5705 bytes
 ...ns__borrow_array__test__emit_get@llvm14.snap |  Bin 0 -> 3190 bytes
 ...rray__test__emit_get@pre-mem2reg@llvm14.snap |  Bin 0 -> 4379 bytes
 11 files changed, 1431 insertions(+), 252 deletions(-)`

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.

Nearly there....done a bunch of mods, hope that's ok, a few questions here

Some(func) => func,
None => {
let arg_tys = args.iter().map(|v| v.get_type().into()).collect_vec();
let sig = match ret_type {
Copy link
Contributor

Choose a reason for hiding this comment

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

bizarre here that BasicTypeEnum and VoidType define fn_type as two completely unrelated methods! But yeah I don't see a better way :(

pub trait BorrowArrayCodegen: Clone {
/// Emit instructions to halt execution with the error `err`.
///
/// This should be consistent with the panic implementation from the [PreludeCodegen].
Copy link
Contributor

Choose a reason for hiding this comment

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

So what you end up doing is parametrizing the struct that implements BorrowArrayCodegen by something that implements PreludeCodegen for this.

I guess the alternative is to define a PanicCodegen super-trait to both PreludeCodegen and BorrowArrayCodegen. The default impl in PreludeCodegen would then go there. Did you consider this? I don't really understand well enough how/where this is all used to know the advantages/disadvantages, but I could do/try that if you think it'd be better?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did you consider this?

No, I just went with this approach since we also used it for e.g. IntCodegenExtension. But I wouldn't mind changing to the super trait approach

}

/// Emit a [`hugr_core::std_extensions::collections::array::ArrayOp`].
/// Emit a [`hugr_core::std_extensions::collections::borrow_array::BArrayOp`].
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be called emit_barray_op? and similarly ...._unsafe_op and emit_barray_{clone, discard, repeat} etc. below?

Copy link
Contributor

Choose a reason for hiding this comment

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

If we keep them as array that would ease recombining via trait BorrowArrayCodegen: ArrayCodegen although I'm not sure whether the change in parametrization would prevent that - I think you might need trait GenericArrayCodegen<AK : ArrayKind> which then makes default-impls hard

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this should all be barray, silly copy paste mistakes sorry 😅

&ctx.typing_session(),
ptr.get_type().get_element_type().try_into().unwrap(),
);
let array_v = array_ty.get_poison();
let array_v = ctx
.builder()
.build_insert_value(array_v, ptr.as_basic_value_enum(), 0, "")?;
let array_v =
ctx.builder()
.build_insert_value(array_v, mask_ptr.as_basic_value_enum(), 1, "")?;
Copy link
Contributor

Choose a reason for hiding this comment

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

The "" for name seems standard throughout the repo, but would strings like "elem_ptr", "bitmask_ptr" and "offset" help with debugging or cause problems? (There are more things to have to look up here...)

Copy link
Contributor

Choose a reason for hiding this comment

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

We use strings for build_extract_value after all!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this would probably be a good thing to do

builder: &Builder<'c>,
array_v: BasicValueEnum<'c>,
) -> Result<(PointerValue<'c>, IntValue<'c>)> {
) -> Result<(PointerValue<'c>, PointerValue<'c>, IntValue<'c>)> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I lean towards a struct BArray(Fat)PointerFields<'a> { array_ptr: PointerValue<'a>, mask_ptr: PointerValue<'a>, array_offset: IntValue<'a> } for return value here and arguments to build_fat_pointer above. Any objection? I can do that but would call the first elems_ptr.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I think there is also the option to implement BasicValue for this struct (similar to how we do sum types)

.build_bit_cast(mask_ptr, usize_t.ptr_type(AddressSpace::default()), "")?
.into_pointer_value();
// Initialise mask using memset
let memset_intrinsic = Intrinsic::find("llvm.memset").unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

I've done this

)
.unwrap();
let val = if value {
ctx.iw_context().i8_type().const_all_ones()
Copy link
Contributor

Choose a reason for hiding this comment

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

I've documented that this argument is always an i8t after breaking everything by trying usize_ty(&ctx.typing_session()) here instead ;)

let block_size = usize_t.const_int(usize_t.get_bit_width() as u64, false);
let builder = ctx.builder();
let block_idx = builder.build_int_unsigned_div(idx, block_size, "")?;
let block_ptr = unsafe { builder.build_in_bounds_gep(mask_ptr, &[block_idx], "")? };
Copy link
Contributor

Choose a reason for hiding this comment

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

Rust code that builds unsafe binary code is itself marked unsafe....!!! Lol, ok

let bit = builder.build_int_truncate(block_shifted, ctx.iw_context().bool_type(), "")?;

let borrowed_bb =
ctx.build_positioned_new_block("elem_borrowed", None, |ctx, borrowed_bb| {
Copy link
Contributor

Choose a reason for hiding this comment

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

I do wonder if these closures are, and this much generality is, a bit OTT...I switched out the if...get_terminator check to put the build_return(None) in the caller, that saved a bit. But passing two Option<&str> panic messages would probably save a bunch more, or one panic message and a bool 'expected_result' (so it's check_bitmask_eq) which would allow a for loop to build two basic blocks...any objections to the latter approach?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also thinking that if we kept a closure (for the success block) we could put the mask-flipping into the same block, which would avoid redoing all the block-index-calculation stuff in build_mask_flip (both at runtime, and duplicating the Rust code here)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

any objections to the latter approach?

No, that sounds good 👍

we could put the mask-flipping into the same block

Also happy with this

})?;

let head_block = ctx.build_positioned_new_block("", Some(body_block), |ctx, bb| {
let (body_start_block, body_end_block, val) =
Copy link
Contributor

Choose a reason for hiding this comment

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

The extra values here are to allow the closure to add new blocks/branches/controlflow/etc. rather than only being able to emit instructions inside one block, right??

So does this build_loop generalize the old one (and we can remove the old from from array.rs?)

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.

hugr-llvm: Add direct lowering for borrow arrays
4 participants