-
Notifications
You must be signed in to change notification settings - Fork 14
feat: LLVM lowering for borrow arrays using bitmasks #2574
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is
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
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:
|
hugr-llvm/src/emit/func.rs
Outdated
let func = ctx | ||
.get_current_module() | ||
.get_function(func_name) | ||
.map(Ok::<_, anyhow::Error>) |
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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?
There was a problem hiding this 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)
hugr-llvm/src/emit/func.rs
Outdated
/// 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>( |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, rational -> rationale
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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`] |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
d82bb30
to
ba4df3d
Compare
|
91f42b2
to
439f09c
Compare
Apologies for force-push but I made sure the old/new commits were identical by The first commit bdc4795 duplicates
|
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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]. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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`]. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, "")?; |
There was a problem hiding this comment.
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...)
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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>)> { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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], "")? }; |
There was a problem hiding this comment.
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| { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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) = |
There was a problem hiding this comment.
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
?)
Closes #2551
The first two commits just copy the existing lowering. The changes to the lowering lgic are implemented in 888e457.