Skip to content
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

Merged
merged 11 commits into from
Jun 21, 2018
Merged

[RFC] [Proposal] Programmatic Port Creation #833

merged 11 commits into from
Jun 21, 2018

Conversation

jackkoenig
Copy link
Contributor

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 and MultiIOModules, 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 in IO.

LegacyModule remains unchanged, although .suggestName becomes an error if you try it on clock, reset, or io in a LegacyModule.

Type of change: other enhancement

Impact: API addition (no impact on existing code)

Development Phase: proposal/implementation

Release Notes

@ducky64
Copy link
Contributor

ducky64 commented Jun 9, 2018

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 @chiselName. I don't quite like suggestName.

Potentially related: issue #666, where string interpolation of IO names (or generally, parameterized IO names) was a requested feature. setName was also thrown around here, but that also sounds more imperative/mutable than declarative/immutable.

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?

@terpstra
Copy link
Contributor

terpstra commented Jun 14, 2018

So the use-case where I want to use this is:

class HelperThing(index: Int) {
  val foo = IO(new Bool)
  foo.suggestName(s"foo_${index}")
  foo := true.B
}

class Top extends Module {
  val helper = new HelperThing(42)
}

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.

@colinschmidt
Copy link
Contributor

@ducky64 Is the concern you have with suggestName that the name is not good, or that you would prefer a less imperative API?

I think this feature makes perfect sense, and its on the user to avoid losing access to their IOs.

chick
chick previously approved these changes Jun 14, 2018
Copy link
Contributor

@chick chick left a 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

@terpstra
Copy link
Contributor

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.

@jackkoenig
Copy link
Contributor Author

Sorry yeah, was meaning to add that today

@terpstra
Copy link
Contributor

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.

@ducky64
Copy link
Contributor

ducky64 commented Jun 14, 2018

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?

@terpstra
Copy link
Contributor

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.

@jackkoenig
Copy link
Contributor Author

I definitely eventually want to have ValName variants on Reg, Wire, etc. and possibly even operations like +. It's a pain to add it everywhere though. For Chisel codebase reasons, maybe we could make a def macro that does all the boiler plate of final def blah = macro do_blah...

Because @chiselName adds .suggestName to everything, the stricter
behavior just breaks the annotation on all LegacyModules
@ducky64
Copy link
Contributor

ducky64 commented Jun 14, 2018

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 makeReg(...), we just write Reg(...)).

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.

@ducky64
Copy link
Contributor

ducky64 commented Jun 14, 2018

ValName: would be interesting, though I thought that Scala wanted to clamp down on macros' ability to inspect outer scopes?
We could certainly write macros to write macros, though there are a few plumbing issues that we would need to discuss (how to deal with inheritance overrides, how to specify the implicit parameters without new people looking at the code and immediately going 'wtf').

@jackkoenig jackkoenig dismissed chick’s stale review June 14, 2018 22:02

Major changes, re-review requested

@jackkoenig
Copy link
Contributor Author

I've added IO as an experimental API that can be called outside of Module definitions.

I also prototyped a different way of implementing the package aliasing, I think the val type thing is kind of weird in the ScalaDoc, so this presents an alternative way where the scaladoc for object IO will actually be in package chisel3.experimental, thoughts? If people like this I think we should refactor the entire API and make things in chisel3.core private wherever possible

@terpstra
Copy link
Contributor

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?

@ducky64
Copy link
Contributor

ducky64 commented Jun 15, 2018

Notes from today's meeting discussions:

  • People want this merged ASAP.
  • My main concern with naming is that it will break user code if we change it. I'm happy with coming up with a new name now, implementing using that, but deferring the stricter checking implementation until the next milestone release.
    • Basically decide on the stuff we can't be agile about right now, while worry about the lower-impact changes later - on the assumption that the power users who are going to be using an experimental API on a non-milestone release will probably be following PRs and understanding limitations.
  • Strawman naming APIs (let's come to a consensus on one?):
    • IO(example).Name(nameString, IO(example).name(nameString)
    • Name(IO(example), nameString), Named(IO(example), nameString)
  • Another variant proposed during the meeting today: IO(example)(nameString) (second argument list is optional, something like ValNamer could automatically populate that unless the user overrides it)

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.
@jackkoenig
Copy link
Contributor Author

I've borrowed ValName from rocket-chip and illustrated how it could be used with the experimental IO. Currently, it looks like: IO(example)(ValName(nameString)), just as a starting proposal.

@jackkoenig
Copy link
Contributor Author

One potential issue with ValName is it doesn't look like it can be explicitly set in the system with macros adding the implicits (as done on UInt operators for example) in one line at least. If we added to those operators one would have to write:

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 (18.U + 13.U).Name("hi") still makes sense

@ducky64
Copy link
Contributor

ducky64 commented Jun 16, 2018

Wait, why can't the implicits be passed explicitly?

I think that for a user-facing API, something more explicit like (18.U + 13.U).Name("hi") (compared to an implicit parameter list) would make more sense. Is it also possible for def macros to insert that (or were there significant restrictions on where you could modify the tree)?

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.

@jackkoenig
Copy link
Contributor Author

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 UInt.+). The whole point of the macro is that an additional parameter list is interpreted as calling .apply so it necessarily prevents passing the implicits explicitly.

@jackkoenig
Copy link
Contributor Author

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 suggestName is a bit beyond the scope too--suggestName is already a public API that is in use. Since this is an experimental API, changing it in the future to require something other than suggestName isn't unreasonable.

@ducky64
Copy link
Contributor

ducky64 commented Jun 18, 2018

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.

@terpstra
Copy link
Contributor

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.

Copy link
Contributor

@ducky64 ducky64 left a 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) {
Copy link
Contributor

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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 = {
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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,

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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 = {
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

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

@jackkoenig
Copy link
Contributor Author

@ducky64 I added some clarifying tests and incorporated some of your feedback!

Copy link
Contributor

@ducky64 ducky64 left a 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.

@jackkoenig jackkoenig added this to the 3.2.0 milestone Jun 20, 2018
@jackkoenig jackkoenig merged commit 980778b into master Jun 21, 2018
@jackkoenig jackkoenig deleted the prog-ports branch June 21, 2018 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants