-
Notifications
You must be signed in to change notification settings - Fork 602
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
[RFC] [Proposal] Programmatic Port Creation #833
Conversation
Allowed in RawModule and MultIOModule
This seems reasonable. I'd like to see wider consensus on a naming API, since I don't think we've formalized on one after Potentially related: issue #666, where string interpolation of IO names (or generally, parameterized IO names) was a requested feature. One major issue: if IOs aren't a val, they aren't externally accessible by member access, until getAllIoAsRecord (or similar) happens. But even then, you'd still be dealing with stringly-typed fields, so that doesn't seem to be a good fit. The main use seems to be generating Modules that need to conform to an external API (for example, Chisel blocks that will be instantiated by Verilog)? It might also be useful for generating BlackBox/ExtModule interfaces, but you would also need to access the IOs from Chisel, which isn't possible if they aren't vals? |
So the use-case where I want to use this is:
The real scenario where I want to use this is obviously part of a much larger design. The intent is to be able to factor the complexity of a very large Module. We've done this in the past with traits, but following the OOP mantra of "Composition over inheritance", the above approach scales better. In particular, it let's us build 'HelperThing' itself out of re-usable bits of code and you can instantiate it more than once in Top. As you can see, there is no concern about losing connectivity, because you always ask for top.helper.foo. It's still accessible, just not as a direct member variable of the module. |
@ducky64 Is the concern you have with I think this feature makes perfect sense, and its on the user to avoid losing access to their IOs. |
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 ok to me. Is there any documentation in the wiki or anywhere else
This PR still needs to make 'IO' some global object, not a protected member variable of Module. Otherwise, it will still be a total PITA to use. |
Sorry yeah, was meaning to add that today |
Regarding the 'suggestName', I don't know if you guys have seen the 'ValName' API used in rocket-chip. The 'IO' function seems a perfect candidate to use that API. You want to require it to be bound to a val anyway. The name also doesn't need to be passed deeply to helper methods. I think ValName variants of 'Reg', 'Wire', and 'IO' would have been a better way to go than @chiselName. |
I understand the use case now - though it might make sense for this to be an experimental feature if possible (much like with MultiIOModule)? It makes sense in theory, but its unclear if there will be issues in practice? I would also prefer naming to be as invisible to the user as possible, keeping with the current style. Can we use reflection to recurse into objects? Also, should the generated name be fully path qualified (and closer to how you would access it from Scala), and is that possible? |
For the place where I need this functionality, having to make an explicit suggestName call is totally acceptable. Attempting to use recursive reflection in my situation is also unlikely to work, because the object relationship is not quite as simple as I presented it. |
I definitely eventually want to have ValName variants on |
Because @chiselName adds .suggestName to everything, the stricter behavior just breaks the annotation on all LegacyModules
With regards to method naming (hello, bikeshedding): I don't like suggestName because it seems more to be a naming hint, and conveys that the system can discard user input (which would seem to be an antipattern). Something that's more of "use this name, or crash" would make more sense to me. And something more declarative would be nice, keeping in line with the rest of the chisel API (we don't write I think it's worth thinking about whether this could fit into any of the existing naming systems, just to reduce the learning curve. Currently, it seems that we have basic chisel (which we've put a lot of effort into reducing the learning curve and providing tutorials), and then you have rocket-chip with the learning curve of a high brick wall, because it uses a bunch of really advanced features. Yes, I know they're needed for a reason, but I'd like to see if there's a way to start unifying things. |
ValName: would be interesting, though I thought that Scala wanted to clamp down on macros' ability to inspect outer scopes? |
I've added I also prototyped a different way of implementing the package aliasing, I think the |
I've just tried using this PR in my work-in-progress branch in fpga-shells. You can see how nicely it has cleaned up the VC707Shell Module in sifive/fpga-shells@e2c1d97 The 'Overlays' are the pieces I add together to make the Shell. Ultimately, since suggestName is an already existing API, that works on wires (and was just ignored on IOs), sticking with suggestName is the least invasive form of naming in this context. Perhaps a better API for the name could be something in a follow-up PR which also tackles the larger naming issue? |
Notes from today's meeting discussions:
|
Initially, ValName is experimentally only used by the experimental version of IO. It provides both the ability to get names from vals without reflection, while also providing a way to set the name explicitly.
I've borrowed ValName from rocket-chip and illustrated how it could be used with the experimental IO. Currently, it looks like: |
One potential issue with val x = {
implicit val y = ValName("hi")
18.U + 13.U
} As far as I can tell, it's not possible to pass the macro added implicits explicitly (which I think is a feature with SourceInfo and CompileOptions. This doesn't mean it wouldn't still be incredibly useful, but perhaps an API like |
Wait, why can't the implicits be passed explicitly? I think that for a user-facing API, something more explicit like I also don't think it's a great idea to have the implicit parameter list explode. We currently have SourceInfo and CompileOptions - neither of which users should specify manually, and it might make sense to be consistent in that none of the implicit parameters should (or could?) be manually specified. |
What I mean is I don't think it's possible to manually pass implicits that are added by a def macro (for example, on |
I think ValName is beyond the scope of this PR so I'm going to remove it. I also agree with @terpstra that coming up with a replacement for |
Sure, renaming the naming APIs can be its own issue. Possibly combined with ValName. As long as everyone using this is fine with getting deprecation warnings or (less likely, because suggestName is already in use) broken code when naming gets fixed. |
I'm totally fine with using an experimental API that may disappear later. All I care about is being able to declare ports programmatically, somehow. The place where I'd use this is nicely abstracted into a shared piece of code anyway. |
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.
Mostly looks fine, but I think there's some implementation details that might warrant a bit more attention? In particular, name conflicts, which I think namespace
resolves by appending digits. Might also want a testcase to make the conflict behavior explicit.
port.suggestName("foo") | ||
} | ||
|
||
class PortAdder(module: NamedModuleTester, name: String) { |
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.
Can you add a comment here indicating the class compositional pattern (I initially interpreted this as a roundabout way to have shared functions)
@@ -79,6 +79,42 @@ object Module { | |||
def currentModule: Option[BaseModule] = Builder.currentModule | |||
} | |||
|
|||
private[chisel3] trait IOImpl { |
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.
Does this need to be a trait, or can this just be an object? It doesn't appear you're extending the trait anywhere, and you should be able to forward vals in the package object?
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 trait is extended by a private object IO used internally to chisel3.core and by an object in package chisel3.experimental (in package.scala). I was kind of prototyping a new way of doing the val forwarding where the in the ScalaDoc the actual documentation exists where you expect it to instead of having to go look in chisel3.core. I grant that perhaps this PR isn't the appropriate place to try out changing how the documentation exists in the ScalaDoc so I'll switch to the existing pattern.
@@ -38,17 +38,22 @@ abstract class UserModule(implicit moduleCompileOptions: CompileOptions) | |||
|
|||
val compileOptions = moduleCompileOptions | |||
|
|||
private[chisel3] def namePorts(names: HashMap[HasId, String]): Unit = { |
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.
Does this detect naming conflicts? Previously we didn't need to worry about this because (presumably) you can't get a conflict with member accessors, but given arbitrary strings, I think this is where you want to error out where you have two wires fighting for the same name
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.
Currently you can get conflicts using suggestName:
class TestModule extends Module {
val io = IO(new Bundle { })
val w = WireInit(10.U).suggestName("foo")
val w2 = WireInit(10.U).suggestName("foo")
}
gives
circuit TestModule :
module TestModule :
input clock : Clock
input reset : UInt<1>
output io : {}
wire foo : UInt
foo <= UInt<4>("h0a")
wire foo_1 : UInt
foo_1 <= UInt<4>("h0a")
This PR doesn't change anything about that.
That being said, there definitely should be some tests illustrating how conflicts are resolved. TLDR, ports always win over things like wires, but _# is introduced when ports conflict as well.
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 think it makes sense to check for conflicts in namePorts and error out if one is detected? I don't know if it makes sense to refactor namespace to do that checking, but that's definitely out of scope for this,
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 think a stricter .Name
API could error. I don't think we should change .suggestName
to error since it's name suggests it won't, but maybe I can make it error just for ports? I'll look into how hard that is 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.
Yeah, I think it should error for ports. We don't have to worry about wires until suggestName is replaced, but I think that a particularly nasty case could be a conflict between a suggested name and a val name, and one of them (probably based on call order) will unexpectedly turn into the wrong thing in the generated verilog.
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 now errors for ports getting suggested (or otherwise named) to the same thing
@@ -170,6 +175,13 @@ abstract class LegacyModule(implicit moduleCompileOptions: CompileOptions) | |||
names | |||
} | |||
|
|||
private[chisel3] override def namePorts(names: HashMap[HasId, String]): Unit = { |
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.
Does LegacyModule really needs its own specialized code path? I think LegacyModule already checks for extraneous IOs as part of existing code, so suggestName-based IOs should already be rejected. In the case of a suggestName("io") conflict, hopefully the name conflict mechanism should cause it to error out?
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 probably doesn't, I'll look 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 actually does, the regular UserModule flow involves checking suggestedName and LegacyModule
needs to not do that. I added a test and then tried merging them but decided to keep them separate
@ducky64 I added some clarifying tests and incorporated some of your feedback! |
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
I see why suggestName needs to be ignored for LegacyModule instead of relying on conflict detection.
The basic idea here is that people want to be able to create ports and name them without being required to assign the port to a val and let reflection name it.
The proposed API is that in
RawModules
andMultiIOModules
, ports can be named in the normal way, but now they also support.suggestName
. If no name is suggested, regular reflection will be used to try to name the port. If no name can be found by regular reflection, then it will error because we don't want_T_#
port names. Ports are also not required to be assigned to some class field, they can be used however the user wants, they just must be wrapped inIO
.LegacyModule
remains unchanged, although.suggestName
becomes an error if you try it onclock
,reset
, orio
in aLegacyModule
.Type of change: other enhancement
Impact: API addition (no impact on existing code)
Development Phase: proposal/implementation
Release Notes