-
Notifications
You must be signed in to change notification settings - Fork 309
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
[FIRRTL][ExpandWhens] Allow zero-width wires to be uninitialized #6455
base: main
Are you sure you want to change the base?
Conversation
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.
LGTM, thanks!
Feel free to land after addressing review.
// zero-width wires do not need to be initialized | ||
if (auto op = type_dyn_cast<FIRRTLBaseType>(dest.getValue().getType())) | ||
if (op.getBitWidthOrSentinel() == 0) | ||
continue; |
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.
Comment nit (PR body/commit nit): this is really any zero-width type, not just wires. This does avoid initialization checks for ports, registers, etc., yes?
Mega-comment nit: Capitalization/end sentences with a period. 😉
// CHECK: firrtl.module @ZeroWidthWire() { | ||
// CHECK: %0 = firrtl.wire : !firrtl.uint<0> | ||
// CHECK: } |
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: the PR can skip doing any checks here. It's successful if there's no error.
Though I agree it's useful to do this but I'm not sure it's generally good to create an exception for strong guarantees of initialization requirements for wires, e.g. we might expect |
If this change should be made, it should be reflected in the FIRRTL spec's "Initialization Coverage" section which presently does not have exceptions for zero-width components (wires or otherwise). Injecting a driver from a constant does seem preferable (re:pipeline assumptions and general clarity/goodness), but I'm not sure this change should be made. Can you motivate this a bit more? |
I'm not too familiar with the exact use cases, but I presume it is a quality-of-life change for chisel users to make it easier for them to handle zero-width cases in their generators. |
@dtzSiFive Agreed that this should be on the spec as well.
@mmaloney-sf, @seldridge, and I discussed this with a few weeks ago offline. The basic idea here is to make the initialization requirements consistent between empty bundles and zero-width types. Empty bundles are not required to be initialized and it made sense that other "empty" types shouldn't need to be either. From the generator perspective (and I think similar things would apply to parameterized FIRRTL), when the parameterization gives you the degenerate case of an empty or zero-width type, there are many operations that become illegal to apply so you end up having to conditionalize that code on whatever parameter results in the empty or zero-width type. Connections are often included in this conditional code block and are thus elided in the empty or zero-width case. It ends up being a pain to add an additional case of initialization for the empty or zero-width typed components. Note that trying to initialize the generator types that lead to empty Bundles would be tricky because in the empty case, there are no fields to dot into and you end up having to special case that logic (if it were required). Consistency makes sense here and IMO consistency on the side of not requiring initialization best serves the user. |
Thinking about the parameterized case... that is really something else. E.g., if you have something like the following with completely made-up syntax where
What this PR is doing is bringing the following three representations in line:
Currently However, this really does need some clarity in the FIRRTL spec as the Initialization Coverage section is clearly lacking. |
Not necessarily, it's very likely some of the firrtl for the |
firrtl-spec PR: chipsalliance/firrtl-spec#156 |
Thanks all for the comments and context, and thanks for the spec PR!
Interesting! I'll take your word for it, but two things FWIW:
|
Consistency across things that have zero-width, you mean? I'm not sure I agree this is overall more consistent or why that's the framing over which consistency is measured. This makes initialization of UInt's inconsistent based on their width, for example. If in FIRRTL you initialized the individual bits of a UInt and thereby implicitly initialized the entire thing, I think this would be making it more consistent. I'm also wondering if this means you can drive a zero-width signal on one branch but not another and that's, "fine" actually? Looks like currently no, but with this PR "yes" (this also answers the question "how does a signal infer to zero-width without being driven?"): circuit ZWCond:
module ZWCond:
input c : UInt<1>
wire w : UInt
when c:
w <= UInt<0>(0) To be fair apparently you can do this for empty-length aggregates to so maybe everything is weird: circuit EmptyVecCond:
module EmptyVecCond:
input c : UInt<1>
input vin : UInt<1>[0]
wire v : UInt<1>[0]
when c:
v <= vin |
If you think about width inference as type inference I don't think it's that weird.
You are correct that that's a reason for zero-width wires and the motivation is the same here. You need zero-width wires to work as expected under all operations to avoid the need to special case. For example avoiding special casing requires reductions (or, xor, and) to be defined, use as a dynamic index to be legal, connecting to and from zero-width wires to be legal. All of these are the case, so now it's just a question of the legality of not driving a zero-width wire. Taking a step back and thinking about it from first principles. What is the purpose of initialization checking? The goal is to avoid accidental undefined behavior (forcing the user to opt-in via the invalidation operation). Zero-width wires (and empty aggregates) do not have any dynamic behavior, there is only 1 value they can possibility represent so there is no undefined behavior to guard against.
For empty aggregates it's even more important. With a Vec, you often iterate on the elements to assign things. If the size is 0, there's nothing to iterate on so no connection is emitted. Obviously that's not something that happens with |
Arguably the historic reason one didn't have to initialize an empty aggregate was there was no way of writing aggregate literals. Why not instead solve this problem by requiring all things to be initialized instead of exempting more things from the rule? |
While applies to some cases, it doesn't apply to all. It is a very common coding pattern in Chisel to iterate on the elements of a Vec and connect each member in the loop. In many of these cases, you don't need to "initialize" the Vec because you're connecting to each member in the 1 place you want to. But in cases where the parameterization may result in the size of the Vec being zero, you'd then have to special case initialize the Vec when size == 0 even though there is no need to do it for any other parameterization. For example: myVec.zipWithIndex.foreach { case (elt, idx) =>
// Note it's common to have lots of other connections in here too
elt := somethingElse(idx) + coolFunction(idx, ...)
} Requiring size 0 Vecs to be initialized would require us to add a special case initialization: if (myVec.isEmpty) {
myVec := DontCare
} The issue here is what is the point? The purpose of initialization checking is to remove a class of undefined behavior. There is no undefined behavior for empty Aggregates nor zero-width wires. There are no bugs being caught by requiring initialization here, instead there's just unnecessary boilerplate. Worse, Chisel users will be encouraged to just blanket DontCare the Vec rather than doing the precise special case I have above, they'll probably just add TLDR Requiring initialization checking for empty aggregates (especially empty Vecs) provides no actual benefit while forcing the user to special-case their Chisel in some circumstances. |
So backing out from the empty aggregate issue, uninitialized things (kind of: https://github.com/chipsalliance/firrtl-spec/blob/main/spec.md#invalidates) generally have indeterminate values (https://github.com/chipsalliance/firrtl-spec/blob/main/spec.md#indeterminate-values). There are plenty of problems right now with code making assumptions about the implementation of indeterminate values. This is trying to make the corner case of knowing that there is only one possible value the same as saying the thing has that value. This adds irregularity to the IR and complicates the safety checks in the code. Neither of these is desirable. When is this going to come up? If I don't know the width of something because it's either uninferred or it's based on a scala value, then in general I'm going to have to initialize the value to something. Saying there is an exception if it is zero would require writing special-cased code to deal with 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 would much rather be consistent in the handling of the integer types.
IR should be consistent and strive towards fewer special cases. Having exactly 2 integer types which are special cased here (and possibly other places when this is tried at scale) to avoid an invalidate is very much a special case.
Don't emit uninitialized errors for zero-width wires