-
Notifications
You must be signed in to change notification settings - Fork 22
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
wip: hir refactoring #332
base: next
Are you sure you want to change the base?
wip: hir refactoring #332
Conversation
|
||
Values have _uses_ corresponding to operands or successor arguments (special operands which are used | ||
to satisfy successor block parameters). As a result, values also have _users_, corresponding to the | ||
specific operation and operand forming a _use. |
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.
specific operation and operand forming a _use. | |
specific operation and operand forming a _use_. |
… grouped entity storage
This commit moves away from `derive!` for operation definitions, to the new `#[operation]` macro, and implements a number of changes/improvements to the `Operation` type and its related APIs. In particular with this commit, the builder infrastructure for operations was finalized and started to be tested with "real" ops. Validation was further improved by typing operands with type constraints that are validated when an op is verified. The handling of successors, operands, and results was improved and unified around a shared abstraction. The pattern and pattern rewriter infrastructure was sketched out as well, and I anticipate landing those next, along with tests for all of it. Once patterns are done, the next two items of note are the data analysis framework, and the conversion framework. With those done, we're ready to move to the new IR.
hir2/src/patterns/applicator.rs
Outdated
} | ||
} | ||
|
||
pub fn match_and_rewrite<A, F, S>( |
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.
This looks astonishing! Looking forward to RewritePattern
impl example.
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 gotten virtually of the rewrite infra built now, but the last kink I'm working out is how to approach the way the Listener
classes are used in MLIR. In effect, the way it is used in MLIR relies on mutable aliases, which is obviously a no go in Rust, and because of that (and I suspect somewhat as an artifact of the design they chose), the class hierarchy and interactions between builders/rewriters/listeners is very confusing.
I've untangled the nest a bit in terms of how it actually works in MLIR, but its a non-starter in Rust, so a different design is required, but one that ideally achieves similar goals. The key thing listeners enable in MLIR, is the ability for say, the pattern rewrite driver, to be notified whenever a rewrite modifies the IR in some way, and as a result, modify its own state to take those modifications into account. There are other things as well, such as listeners which perform expensive debugging checks without those checks needing to be implemented by each builder/rewriter, but the main thing I care about is the rewrite driver.
There are a couple of approaches that I think should work, but need to test them out first. In any case, that's the main reason things have been a bit slow in the last week.
} | ||
} | ||
|
||
derive! { |
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 verification subsystem is fascinating! Great job! There is a lot of macros and type-level magic going on here. I must admit that after reading all the extensive documentation, I still don't fully understand how it works. I'm fine with this level of complexity, but looking at the derive!
macro usage, I'm not sure that macro's benefits outweigh the complexity. I mean this derive!
call expands to the following code:
#[doc = r" Op's regions have no arguments"]
pub trait NoRegionArguments {}
impl<T: crate::Op + NoRegionArguments> crate::Verify<dyn NoRegionArguments> for T {
#[inline]
fn verify(&self, context: &crate::Context) -> Result<(), crate::Report> {
<crate::Operation as crate::Verify<dyn NoRegionArguments>>::verify(
self.as_operation(),
context,
)
}
}
impl crate::Verify<dyn NoRegionArguments> for crate::Operation {
fn should_verify(&self, _context: &crate::Context) -> bool {
self.implements::<dyn NoRegionArguments>()
}
fn verify(&self, context: &crate::Context) -> Result<(), crate::Report> {
#[inline]
fn no_region_arguments(op: &Operation, context: &Context) -> Result<(), Report> {
for region in op.regions().iter() {
if region.entry().has_arguments() {
return Err(context
.session
.diagnostics
.diagnostic(Severity::Error)
.with_message("invalid operation")
.with_primary_label(
op.span(),
"this operation does not permit regions with arguments, but one was \
found",
)
.into_report());
}
}
Ok(())
}
no_region_arguments(self, context)?;
Ok(())
}
}
Which is not that larger than the macro call itself. If part of it can be generated by an attribute macros it's not that hard to write the rest of it by hand (Verify::verify
only?). I mean, the benefits of derive!
macro might not outweigh the drawbacks (remembering the syntax, writing code inside a macro call, etc.).
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.
Yeah I mostly didn't want any of the type-level magic (and boilerplate) to be hand-written as things were evolving - modifying the macro and updating all of the traits at once was far easier, and is really the only reason why the derive!
macro still exists. However, as you've pointed out, it would be best to define an attribute macro for this as well, just like #[operation]
, but I've punted on that since the derive!
macro gets the job done for now, and not many new op traits will be getting defined in the near term, but its certainly on the TODO list.
hir2/src/lib.rs
Outdated
#![feature(allocator_api)] | ||
#![feature(alloc_layout_extra)] | ||
#![feature(coerce_unsized)] | ||
#![feature(unsize)] | ||
#![feature(ptr_metadata)] | ||
#![feature(layout_for_ptr)] | ||
#![feature(slice_ptr_get)] | ||
#![feature(specialization)] | ||
#![feature(rustc_attrs)] | ||
#![feature(debug_closure_helpers)] | ||
#![feature(trait_alias)] | ||
#![feature(is_none_or)] | ||
#![feature(try_trait_v2)] | ||
#![feature(try_trait_v2_residual)] | ||
#![feature(tuple_trait)] | ||
#![feature(fn_traits)] | ||
#![feature(unboxed_closures)] | ||
#![feature(const_type_id)] | ||
#![feature(exact_size_is_empty)] |
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.
There goes my hope to someday ditch the nightly. :)
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.
Yeah I've really blown a hole in that plan 😂. Once you need some of the nigh perma-unstable features like ptr_metadata
, fn_traits
, or specialization
/min_specialization
, it's basically game over for using stable, unless you have alternative options that allow you to sidestep the use of those features.
That said, a fair number of these are pretty close to stabilization. The ones that are problematic are:
specialization
andrustc_attrs
are needed becausemin_specialization
isn't quite sufficient for the type-level magic I'm doing for automatically deriving verifiers for operations. If improvements are made tomin_specialization
, we can at least switch over to that. Longer term, we'd have to come up with an alternative approach to verification if we wanted to switch to stable though.ptr_metadata
has an unclear stabilization path, and while it is basically table stakes for implementing custom smart pointer types, stabilizing it will require Rust to commit to some implementation details they are still unsure they want to commit to.allocator_api
andalloc_layout_extra
should've been stabilized ages ago IMO, but they are still being fiddled with. However, we aren't really leaning on these very much, and may actually be able to remove them.coerce_unsized
andunsize
are unlikely to be stabilized any time soon, as they've been a source of unsoundness in the language (not due to the features, but due to the traits themselves, which in stable are automatically derived). Unfortunately, they are also table stakes in implementing smart pointer types with ergonomics like the ones in libstd/liballoc, so its hard to avoid them.try_trait_v2
andtry_trait_v2_residual
are purely ergonomic, so we could remove them in exchange for more verbose code in a couple places, not a big dealfn_traits
,unboxed_closures
, andtuple_trait
are all intertwined with the ability to implement implementations ofFn
/FnMut
/FnOnce
by hand, which I'm currently using to make the ergonomics of op builders as pleasant as possible. We might be able to tackle ergonomics in another way though, and as a result, no longer require these three features.
So, long story short, most of these provide some really significant ergonomics benefits - but if we really wanted to ditch nightly for some reason, we could sacrifice some of those benefits and likely escape the need for most of these features. Verification is the main one that is not so easily abandoned, as the alternative is a lot of verbose and fragile boilerplate for verifying each individual operation - that's not to say it can't be done, just that I'm not sure the tradeoff is worth it.
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 can live with it. The verification is totally worth it!
/// else_region: RegionRef, | ||
/// } | ||
#[proc_macro_attribute] | ||
pub fn operation( |
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 really dig the operation
proc macro attribute. I expanded it in a few ops. So much pretty complex boilerplate is generated. Awesome!
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 don't know if you say the derive!
macro before I implemented the #[operation]
attribute macro, but...it was madness 😅.
Now it is much cleaner (and more maintainable!), but it does a lot under the covers, so the main tradeoff is lack of visibility into all of that. That said, I tried to keep the macro focused on generating boilerplate for things that feel pretty intuitive, e.g. deriving traits, op construction and verification, correspondence between fields of the struct definition and the methods that get generated. So hopefully we never need to actually look at the generated code except in rare circumstances.
hir2/src/ir/operation/name.rs
Outdated
} | ||
|
||
#[inline(always)] | ||
unsafe fn downcast_ref_unchecked<T: Op>(&self, ptr: *const ()) -> &T { |
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'm a fan of op traits implementation! TIL about DynMetadata
. So cool!
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'm definitely happy with how that has turned out so far, it enables a number of really neat tricks! You might also be interested in a bit of trivia about pointer metadata in Rust:
DynMetadata<dyn Trait>
is the type of metadata associated with a trait object for the traitTrait
, and is basically just a newtype for a pointer to the vtable of the trait object. While you could specifyDynMetadata<T>
for someT
that isn't a trait, that doesn't actually mean anything, and is technically invalid AFAIK.usize
is the type of metadata associated with "unsized" types (except trait objects), and corresponds to the unit size for the pointee type. For example,&[T]
is an unsized reference, and its pointer metadata is the number ofT
in the slice, i.e. a typical "fat" pointer; but the semantics of the metadata value depend on the type, so you can have custom types that useusize
metadata to indicate the allocation size of the pointee, or something else along those lines.()
is the type of metadata associated with all "sized" types, i.e. pointers to those types don't actually have metadata
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.
Thank you! I'm definitely going to dig into it.
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.
It looks super cool! I'm looking forward to using it!
Thanks for reviewing things, and for the kind comments! As mentioned, things are quite close to finished now, the last thing that needs some fiddling is finding a Rust-friendly design for handling the interaction between patterns, builders/rewriters, and the rewrite driver (i.e. so that the rewrite driver is made aware of changes to the IR made by a rewrite pattern via the builder/rewriter). Once that's out of the way, there is a short list of tasks to switch over to this new IR:
|
…ication, rework visitors, and implement low-level pattern matchers
This commit adds the following useful tools (used in later commits): * The `Graph` trait for abstracting over flow graphs * The `GraphVisitor` and `DfsVisitor` primitives for performing and hooking into depth-first traversals of a `Graph` in either pre-order or post-order, or both. * Type aliases for pre- and post-order dfs visitors for blocks * The `GraphDiff` trait and `CfgDiff` data structure for representing pending insertions/deletions in a flow graph. This enables incremental updating of flow graph dependent analyses such as the dominator tree.
This introduces the incrementally updateable dominator and post-dominator analyses from LLVM, based on the Semi-NCA algorithm. This enables us to keep the (post-)dominator tree analysis up to date during rewriting of the IR, without needing to recompute it from scratch each time. This is an initial port of the original C++ code, modified to be more Rust-like, while still remaining quite close to the original. This could be easily cleaned up/rewritten in the future to be more idiomatic Rust, but for now it should suffice.
This commit implements the generic loop analysis from LLVM, in Rust. It is a more sophisticated analysis than the one in HIR1, and provides us with some additional useful information that will come in handy during certain transformations to MASM. See the "Loop Terminlogy" document on llvm.org for details on how LLVM reasons about loops, which corresponds to the analysis above.
fn matches(&self, op: OperationRef) -> Result<bool, Report> { | ||
use crate::matchers::{self, match_chain, match_op, MatchWith, Matcher}; | ||
|
||
let binder = MatchWith(|op: &UnsafeIntrusiveEntityRef<Shl>| { | ||
log::trace!( | ||
"found matching 'hir.shl' operation, checking if `shift` operand is foldable" | ||
); | ||
let op = op.borrow(); | ||
let shift = op.shift().as_operand_ref(); | ||
let matched = matchers::foldable_operand_of::<Immediate>().matches(&shift); | ||
matched.and_then(|imm| { | ||
log::trace!("`shift` operand is an immediate: {imm}"); | ||
let imm = imm.as_u64(); | ||
if imm.is_none() { | ||
log::trace!("`shift` operand is not a valid u64 value"); | ||
} | ||
if imm.is_some_and(|imm| imm == 1) { | ||
Some(()) | ||
} else { | ||
None | ||
} | ||
}) | ||
}); | ||
log::trace!("attempting to match '{}'", self.name()); | ||
let matched = match_chain(match_op::<Shl>(), binder).matches(&op.borrow()).is_some(); | ||
log::trace!("'{}' matched: {matched}", self.name()); | ||
Ok(matched) | ||
} | ||
|
||
fn rewrite(&self, op: OperationRef, rewriter: &mut dyn Rewriter) { | ||
log::trace!("found match, rewriting '{}'", op.borrow().name()); | ||
let (span, lhs) = { | ||
let shl = op.borrow(); | ||
let shl = shl.downcast_ref::<Shl>().unwrap(); | ||
let span = shl.span(); | ||
let lhs = shl.lhs().as_value_ref(); | ||
(span, lhs) | ||
}; | ||
let constant_builder = rewriter.create::<Constant, _>(span); | ||
let constant: UnsafeIntrusiveEntityRef<Constant> = | ||
constant_builder(Immediate::U32(2)).unwrap(); | ||
let shift = constant.borrow().result().as_value_ref(); | ||
let mul_builder = rewriter.create::<Mul, _>(span); | ||
let mul = mul_builder(lhs, shift, Overflow::Wrapping).unwrap(); | ||
let mul = mul.borrow().as_operation().as_operation_ref(); | ||
log::trace!("replacing shl with mul"); | ||
rewriter.replace_op(op, mul); | ||
} |
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 Ergonomics looks great! The code gives strong MLIR vibes! It'd take some time to get used to the types, techniques, etc. but it worth it. I'm looking forward to starting writing some rewrites.
TBD