-
Notifications
You must be signed in to change notification settings - Fork 603
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
Module Hierarchy Refactor: Request for Comments #507
Comments
To me, the overriding concern is the stylistic one; I consider it a great feature that the Module's interface is specified in one place. I realize it doesn't work for black boxes, but waveform debugging of Chisel-generated Verilog is usually simplified by module interfaces having the common prefix io_. I would prefer not relying on reflection in even more places. |
One thought is the current cake pattern + single val io forces the user to create an excessive amount of bundle traits, while multiple io declarations could resolve this. Suppose I want to declare a Module trait that back-propagates an input ready signal to an output ready signal. Currently, I'd need to 1) create the Module trait to contain the hardware connection, and 2) create a new Bundle trait that ensures my io has the two ready ports I need. If multiple io's are allowed, then I only need to make the Module trait, which can directly add both ready ports, as well as the connection. No new Bundle trait needed. We see this all the time in rocket-chip, where we have a crazy number of bundle traits. I suspect this is because there is no easy way to append ports to modules, (like there is a way to append hardware to modules via traits). |
I understand the concern of having all io declared in one place, but one could argue that even now, not all io is declared at the top of a module. In rocketchip, we usually instantiate a custom bundle, which abstracts away all of the leaf io ports anyways. If I actually want to know what the ports are, I need to track down wherever that bundle is declared. This is distinctly not having all IO inside the module. Software inheritance intentionally allows users to declare public methods/val's not with the concrete class. I suspect that the benefits of allowing more powerful Module traits outweighs the bad style of randomly declaring a port in the middle of a module. This is something we can certainly code review/style check for. |
Right. The 3-part extension everywhere is kind of tiresome. It would also be really nice to be able to punch ports easily by just mixing in a "debugging ports" trait up the hierarchy. I do second the request to keep things called "io_", but perhaps that now becomes a burden on the designer to follow a naming convention... I assume that if this was enacted, every time I wanted to talk about a module's IO from the parent module, I have to know the right name for that part of the IO? As in (assuming my guess at your syntax is right):
|
Finally, allowing users to declare separate io's would allow them to actually comment their io's. We could enforce an markdown-style comment for io's, which would allow a detailed explanation of the port's behavior. |
"There is a significantly increased possibility of a naming collision with an internal Module wire. There is de-duplication logic in place for that, though it may get triggered more and can impede debuggabitlity. What exactly is the problem here? Can it just be flagged as an error? |
If io_ is not appended, then |
Yeah that is confusing. It mirrors a problem in Verilog but it's less obvious because the r is so deep in some bundle you may not be aware of. Not having the io_r name is suprising to say the least. I guess I would have expected the following to be allowed:
With this it would be pretty confusing to lose the names ioA and fooB in the generated code. Is this what you mean by needing reflection to properly name the ios in verilog? Also, what is the difference between "IO" and just saying "Output" ? What happens if I put something that is not "Input" or "Output" in an IO, or just declare something as "Input" at the top level? (This is more understanding the proposed user view of this) |
I'm super confused. I thought the discussion was to make it so if you just wanted a port val r = IO(Input(Bool())) and that val io = IO(new Bundle { val r = Input(Bool()) }) would give you |
There are multiple proposals going on here.
|
Please don't drop the io_ prefix for Module!! I understand the desire for
RawModule, as a separate feature, but it is an anti-feature in the other
cases, causing you to lose logical grouping of related signals. You
wouldn't flatten a Wire bundle and destroy its name prefix. So it hardly
makes sense to do that (by default) for the most important bundle in a
Module.
…On Tue, Feb 14, 2017 at 9:54 AM Adam Izraelevitz ***@***.***> wrote:
There are multiple proposals going on here.
1. Multiple IOs. Allows you to declare val myIO = IO(...). No dropping
of prefix myIO_.
2. Single IO, required to be declared as val io = IO(...). Dropping
prefix io_.
3. Single IO, required to be declared as val io = IO(...). Keep prefix
io_.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#507 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AA-7wqGRyVDgNcvnnMDTmSTUEDGzZUy8ks5rcepLgaJpZM4L_4tq>
.
|
Side note about use of reflection for IO ports: it's unlikely to fail since to have a useful IO, it needs to be a public object method (otherwise, you can't access it from outside), so it's pretty likely that it will work. There may be edge cases (like override val, but you really shouldn't be doing that anyways). |
I am extremely hesitant of the multiple IO idea and I am not OK with reflection actually becoming a critical feature (rather than nice-to-have). Users extending module already leads to leaky abstractions since they shouldn't actually access any fields except for the IO (and clock and reset). Now, with multiple IOs, there is no clean distinction. The user can access 'any' field that happens to be bound to an IO. This could be anything and isn't tracked in the type system so it can be a bit wild. I disagree that the use of reflection will always work since you are now at the mercy of style guidelines to prevent the following abuses: val stall = if(allowStall) IO(Input(Bool())) else false.asBool // confusing but tempting
val (a, b) = (IO(Input(Bool())), 1) // macro approach will fail to assign name
val a = (IO(Input(Bool())), 1) // macro and runtime approaches will fail to assign name I haven't followed the software engineering architecture of rocket-chip; however, the provided example was somewhat confusing. Rather than mixing in traits to the Module definition, why not instantiate submodules/call helper functions to do such work? The Record approach already allows for programmatically building up a Bundle. (In fact, you could still get your split IO for select modules by defining IO late in the module and building it up as a Record of parts defined earlier) |
I understand your concern, but are you saying there is another way to get what we want (being able to add stuff to IOs when we extend a module. Can we now instantiate submodules and then query what their IOs are and add them to our top-level? What if I want to extend a module like:
Are you saying that with Record I can now add whatever FancyPeripheral's IO is to the IO of OriginalModule's IO? How? |
I don't particularly suggest the approach for readability reasons but it requires the involved modules to have been designed for this purpose (hence why I was looking more at the trait-based approach). The brief thought I had was: I'm not a huge fan of that notion and in the case you proposed would likely prefer having FancyModule instantiate and then connect up the OriginalModule. (Although, even in the inheritance case, you can have a habit of ancestor modules taking an io to use as a constructor argument so that children can enrich the IO with new fields.) I should caveat that, for debugging purposes, I implemented #159 . However, the design is such that addPin is only called outside the other modules (almost always as some sort of library) rather than a thing that can be called by the current module. It is generally used to bubble up an internal signal to monitor in a testbench or force down some input override for testing. |
More on reflection:
In the end, it's a choice of trade-offs between several imperfect solutions. The current way (single |
Perhaps we need some experimentation before we can really understand the implications of multiple IOs. Is it possible to do solution 3 (one IO, keep io_ prefix), but allow multiple IOs if you import experimental? Then, if it turns out people are hanging themselves with too much rope, we can deprecate the feature? |
I think it's possible, but requires a bit more engineering effort - the implementation will probably be much like compile options. Effort aside, I think it's a good intermediate solution until we fully understand the trade-offs we're making. That being said, the BlackBox IO behavior will still be inconsistent, and that will be an API breaking change (unless we create a new BlackBox class or something). |
I suppose we could do:
I vote for 2, then 1, then 3. |
What would you call the new BlackBox? Naming is hard... |
I think BlackBoxes is one place its most unlikely to want to have multiple IO's. |
@ducky64 firrtl calls them external modules |
I think it would be interesting to first push the limits on what we can do with Record. |
Record has its place where things must be programaticly generated, but you lose static typing (where you can catch errors can compile time and IDEs have code assist) so it's much less desirable if you want to do can be expressed staticly. |
I disagree, I think it is totally unnatural to declare your black box ports in the
I think Megan's point that this can be something enforced by the designer is a good counterpoint to this. Another option is that we can add an optional Firrtl pass that appends the "kind" to every name in the design (like io_, reg_, wire_ etc.), for easier waveform viewing.
Agreed, I don't know enough to comment, but it seems like its no worse than Bundle.
Agreed, experimentation is good. |
First, to reiterate with detail, I retain concern about more critical reliance on reflection since I do not think reflection should be considered "unlikely to fail". See the situation leading up to #456 . (Also, that PR remains unmerged so it isn't like we can rely on these reflection issues being fixed promptly.) Also, to reiterate a different point: people do complain (consistently, even while still using Chisel) about all the badly named T and GEN wires. It is very convenient to be able to claim that they are merely internal signals whose name stability shouldn't be relied on too extensively (except perhaps for debugging) but that you can still rely on module IO names to be sane and user-driven. Second, as an analogy, people rarely seem to complain that function arguments and return details have to specified in a singular place. Similarly, users can rely on the module IO at least being in one place (and in one file). A Module can inherit from a trait to do multiple things (which, at this time, does not include adjusting the IO) whereas an IO Bundle will clearly do so to add fields (constraining the user search). Finally this point continues to be oddly asserted, Record does not necessarily lose static typing information in the same way Bundle does not lose static typing information. If all you know it is a Record, sure you don't know the types of the subtype; however, specific Record implementations can retain type information. e.g. class MyRecord extends Record {
val a = UInt(16)
val b = SInt(16)
def elements = ListMap("a" -> a, "b" -> b)
// insert cloneType def here
} Retaining type information across the IO situation is trickier but doable. (The Record would likely have a 'polymorphic map' that contains pointers from the driving data wire in the extending trait and io wire designated to connect to it. This is access with def getIO[T <: Data](desired: T): T. The truly typesafe version technically requires this to return an Option[T]; however, T is likely more usable in actual use cases. You would access these hidden IOs with io.getIO(desiredPartialIOInTrait).) These extractions could even be done as a def inside the trait itself (as long as it is only called by the parent module, namely after the concrete subclass is fully constructed). The example is cursory and could use some refinement/exploration. |
Superceded by #512. |
Turns out the module hierarchy refactor is trickier than we originally thought, and there are several major decision points to be made. Feedback, thoughts, and example code (and especially example code) are welcome!
Allow multiple top-level IOs?
Should multiple top-level IOs be allowed? Currently, we only allow the
io
Bundle, but the infrastructure is partially there to allow IOs other thanio
.Side note:
clock
andreset
are implemented as top level IOs, but often aren't used as such (instead, it's common to useoverride_clock
andoverride_reset
, or soonwithClock
blocks).Pros:
io
.clock
andreset
implementation, though practically users will use the coming-soonwithClock
blocks. With this method,clock
andreset
can be implemented with anImplicitClock
trait that is mixed in to an ImplicitModule, and users can define their ownImplicitX
trait for things they want automatically hooked up.io
scheme.Cons:
val
inside the top level (not in a for loop / if statement) of a module body. This is a restriction of the Scala type system, the IO obviously has to be a class method to be reference-able outside.override val io
would be disallowed and fail to compile (though this is probably a bad idea anyways). The pattern of constrainingio
type in superclasses (without giving it a concrete value, except at the leaf class) would still be fine.Style:
io
Bundle forces all IOs to be defined at once place (good for readability), though style guides may limit how messy multiple top-level IOs can beCan we unify naming of BlackBoxes and ImplicitModule
Currently, BlackBox drops the
io_
prefix when generating names for IO ports, while Modules keep theio_
prefix.In the single top-level IO case, one solution is to drop the
io_
prefix from Modules. Some collisions are possible:clock
andreset
in theio
Bundle would conflict with the implicit clock and reset signals. This could just error out (as having aclock
andreset
member in an IO Bundle is confusing).Dropping the prefix from multiple top-level IOs is probably a really bad idea. In that case, an alternative would be for BlackBoxes to also use multiple top-level IOs to name pins without the
io_
prefix.Leaving it as-is (inconsistent) is a possibility, though undesirable.
Are we OK with IOs requiring reflection?
One implementation for top-level IOs requires reflection to properly name the top-level IOs. Module reflection may be significantly more difficult to do than Bundle reflection, where things are structured uniformly.
That being said, top-level IOs are usually defined similarly to how Bundles define members.
Thoughts?
@sdtwigg @shunshou @aswaterman
The text was updated successfully, but these errors were encountered: