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

Instance/Definition (formerly Template) #2045

Merged
merged 1 commit into from
Sep 5, 2021
Merged

Conversation

azidar
Copy link
Contributor

@azidar azidar commented Jul 21, 2021

Starting to work on an official PR. More edits coming! For now, this is a DRAFT PR.

Documentation

To Decide!

  • (All) Decide on @public. Alternatives: @visible, @open, @accessable, @distinct, @tangible, @gettable, @viewable
  • (All) Shorten Definition to Def , or Instance to Inst ?
  • Should @public work on def? No, because if we will replay, we won't have access to defs
  • Can we expose Definition's underlying module? e.g. for Aspects? For 'replaying' ability, this would be a bad API
  • Decide on Instance vs just using Module -- Using Instance because of need for Instance.wrap
  • Consider making apply method more private on Instance class? -- Yes

To Document!

  • (Adam) Write ScalaDoc for @instantiable and @public
  • (Adam) Add Definition examples to mdoc
  • Write ScalaDoc for Definition/DefinitionLookup
  • Clean up licenses
  • Write most ScalaDoc
  • Write Mdoc

To Test!

  • (Jack) Ensure Lookupable composes with Views
  • (Jack) See if Lookupable composes with HWTuple and implicit conversions
  • Ensure Select APIs error when applied to designs using D/I
  • Test Definition extensively
  • Test Definition accessors and toTargets
  • Support annotations and .toTarget inside of Definition
  • Check what happens if you do multiple . in (class) Instance.apply -- this is probably buggy and needs to be banned by a macro Made it harder to use the apply API, so this is no longer relevant.

To Develop!

  • (Adam+Jack) Add more default Lookupable typeclasses for simple vals (Double, Float etc).
  • (Jack) Fix @public val x = bundle.subfield bug
  • Self-review new code, and refactor if necessary
  • Try generating toTarget method, so Definition[Data] can return ReferenceTarget vs Definition[BaseModule]
  • Definition wrapping black boxes doesn't let you access io.
  • Add Definition.wrap behavior
  • Generate extension accessors for Definition
  • Support reset inference in Definition (instead of defaulting to Bool for top-level modules)
  • Make apply method on Instance class harder to access
  • Make @instantiable put extension methods in companion object
  • Move things to experimental
  • Make extending IsInstantiable optional when you have @instantiable as well
  • More conversions (for containers like Seqs/Option) Replaced by explicit Instance.wrap
  • More lookupables (for containers like Seqs/Option)
  • Make Instance Covariant

To Do Later!

  • (Jack) Dogfood Queue with this API (the companion object apply method that returns Decoupled[T])
  • Try to make @instantiable optional
  • Try to make @public optional on ports
  • Try to make @public work on vals in the class constructor
  • Try to put the @instantiable implicit class in the type's companion object for better implicit resolution
  • Write explanation mdoc (not just cookbook)
  • Consider making module visible from definition, so you can inspect a definition's parameters etc.
  • Consider composition of Definitions outside of a chisel stage. For example, should we have an emitDefinition from a stage?
  • Enable reconstitution of a definition which was elaborated in a separate JVM invocation, by using Instance/Definition as user interface
  • Develop new injection aspect API based upon D/I

Contributor Checklist

  • Did you add Scaladoc to every public function/method?
  • Did you add at least one test demonstrating the PR?
  • Did you delete any extraneous printlns/debugging code?
  • Did you specify the type of improvement?
  • Did you add appropriate documentation in docs/src?
  • Did you state the API impact?
  • Did you specify the code generation impact?
  • Did you request a desired merge strategy?
  • Did you add text to be included in the Release Notes for this change?

Type of Improvement

  • bug fix
  • documentation
  • new feature/API

API Impact

Only adds APIs. However, note that as of this PR, it is incompatible with some Aspect APIs. Future work will introduce new Aspect APIs which are compatible.

The one exception is we change the Builder.build function to accept BaseModule, not just RawModule. However, this change is not exposed to the user.

Backend Code Generation Impact

No affect.

Desired Merge Strategy

  • Squash: The PR will be squashed and merged (choose this if you have no preference.

Or.. I'll rewrite the history? Let me know if you want separate commits.

Release Notes

Prior to this release, Chisel users relied on deduplication in FIRRTL to create multiple instances of the same module. This release's chisel3.experimental.hierarchy package introduces the following new APIs to enable multiply-instantiated modules directly in Chisel.

Definition(...) enables elaborating a module, but does not actually instantiate that module. Instead, it returns a Definition class which represents that module's definition.

Instance(...) takes a Definition and instantiates it, returning an Instance object.

Modules (classes or traits) which will be used with the Definition/Instance api should be marked with the @instantiable annotation at the class/trait definition.

To make a Module's members variables accessible from an Instance object, they must be annotated with the @public annotation. Note that this is only accessible from a Scala sense -- this is not in and of itself a mechanism for cross-module references.

In the following example, use Definition, Instance, @instantiable and @public to create multiple instances of one specific parameterization of a module, AddOne.

import chisel3._
import chisel3.experimental.hierarchy._

@instantiable
class AddOne(width: Int) extends Module {
  @public val in  = IO(Input(UInt(width.W)))
  @public val out = IO(Output(UInt(width.W)))
  out := in + 1.U
}

class AddTwo(width: Int) extends Module {
  val in  = IO(Input(UInt(width.W)))
  val out = IO(Output(UInt(width.W)))
  val addOneDef = Definition(new AddOne(width))
  val i0 = Instance(addOneDef)
  val i1 = Instance(addOneDef)
  i0.in := in
  i1.in := i0.out
  out   := i1.out
}

chisel3.stage.ChiselStage.emitVerilog(new AddTwo(10))

Reviewer Checklist (only modified by reviewer)

  • Did you add the appropriate labels?
  • Did you mark the proper milestone (3.2.x, 3.3.x, 3.4.x, 3.5.0) ?
  • Did you review?
  • Did you check whether all relevant Contributor checkboxes have been checked?
  • Did you mark as Please Merge?

Copy link
Contributor

@ekiwi ekiwi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some thoughts.

core/src/main/scala/chisel3/Data.scala Outdated Show resolved Hide resolved
src/test/scala/chiselTests/InstanceSpec.scala Outdated Show resolved Hide resolved
src/test/scala/chiselTests/InstanceSpec.scala Outdated Show resolved Hide resolved
class AddZeroWithCompanionObject extends MultiIOModule {
@public val in = IO(Input(UInt(32.W)))
@public val out = IO(Output(UInt(32.W)))
@public val clock = clock
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't necessarily like the @public annotation. Wouldn't normally only the I/Os be public? Is there a way to automatically expose the I/Os without requiring a new annotation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like the explicit annotation, but I don't think @public is the right word (it's too generic). instancePublic could be one suggestion, or annotatable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you are saying by default IOs should be public? That may require some cleverness to get working by default, but I can work on it.

Copy link
Contributor

@mwachs5 mwachs5 Jul 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we distinguish between I/Os which might be optimized away and those that might not? Is there any implication if you mark an I/O @to-be-renamed-public on whether it can be optimized away? that may be an argument for NOT automatically making all I/Os @to-be-renamed-public

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimizing away I/Os is a firrtl issue, not really a Chisel issue imho. This means that the firrtl compiler will be able to figure out which I/Os it can get rid of based on how the module is used, Chisel users should not have to worry about that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimizing away I/Os is a firrtl issue, not really a Chisel issue imho

I think this is only true if FIRRTL should never optimize away I/Os but given it's an intentional feature, I think you need a Chisel-level user API to say either when you should, or shouldn't, optimize ports away. Making it such that ports defined as part of the "public API" seems like it could be convenient/intuitive but perhaps I'm wrong here.

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 this is only true if FIRRTL should never optimize away I/Os but given it's an intentional feature, I think you need a Chisel-level user API to say either when you should, or shouldn't, optimize ports away. Making it such that ports defined as part of the "public API" seems like it could be convenient/intuitive but perhaps I'm wrong here.

I guess my stance on this had always been that only toplevel I/Os need to be preserved.

However, I think the discussion of whether there should be a way to annotate I/Os as public API should be independent from the Instance API discussion. I just wanted to point out that during Chisel elaboration nothing is optimized away, so there is no technical reason as far as I can see for Chisel users to be concerned about optimized I/Os in the context of the new Instance API.

@@ -239,12 +275,11 @@ package internal {
rec(start)
}

private[chisel3] def cloneIORecord(proto: BaseModule)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): ClonePorts = {
private[chisel3] def createIORecord[T <: BaseModule](cloneParent: ModuleClone[T])(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): ClonePorts = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this function we check if proto isInstanceOf[Module], changed from MultiIOModule. Note that in the backport, we have to check MultiIOModule, not Module. I saw this as a bug, so just noting it here so we remember.

@jackkoenig
Copy link
Contributor

Squashed to make rebasing easier and rebased on master (which now has DataView). Adam's original work backed up in https://github.com/chipsalliance/chisel3/tree/instance-definition-bak

@jackkoenig
Copy link
Contributor

jackkoenig commented Aug 18, 2021

TLDR I've removed the implicit conversions to Instance[T], replacing them with Instance.wrap (in addition to the fact that Instance is now covariant so things like Seqs and Options work for free.

In writing tests I ran into issues where the implicit conversion, in particular T <: BaseModule => Instance[T] do not work very well in practice (and give very confusing error messages). For example, given something like:

val inst: Instance[MyModule] = Module(new MyModule)

This will give an error of the form:

inferred type arguments [chisel3.experimental.hierarchy.Instance[MyModule]] do not conform to macro method apply's type parameter bounds [T <: chisel3.experimental.BaseModule]
val i: Instance[MyModule] = Module(new MyModule)

This message is almost worthless, and it turns out the problem is that the implicit conversion is inserted inside Module.apply instead of outside of it. I asked about this on Gitter and it's basically a fundamental issue with implicit conversions (https://gitter.im/scala/scala?at=611d69937555e333511aa5a8)

So the implicit conversion work great sometimes, but there are common use cases where they fall on their head, same problem when trying to mix Modules and Instances (which you might do with BlackBoxes)

val template = Definition(new MyModule)
val insts: List[Instance[MyModule]] = List(Module(new MyModule), Instance(template))
// inferred type arguments [chisel3.experimental.hierarchy.Instance[MyModule]] do not conform to macro method apply's type parameter bounds [T <: chisel3.experimental.BaseModule]
//    val insts: List[Instance[MyModule]] = List(Module(new MyModule), Instance(template))

(And note that if I didn't have the explicit type parameter, Scala would infer that as a List[Any])

So to avoid these worthless error messages, I've removed the implicit conversion and added Instance.wrap so you have to explicitly wrap Modules or other IsInstantiables when you need an Instance. I also made BaseModule a subtype of IsInstantiable because I was having issues with type erasure and ambiguous implicits when trying to have 2 versions of it.

I called the method Instance.wrap because I don't want people to confuse making multiple instances of a Definition vs. wrapping a Module as an Instance

val proto = Definition(new MyModule)
val i0 = Instance(proto)
val i1 = Instance(proto)
// i0 and i1 are different instances
val inst = Module(new MyModule)
val i2 = Instance.wrap(inst)
val i3 = Instance.wrap(inst)
// i2 and i3 are the same instance

This change nixes @ekiwi's suggestion that we drop Instance in favor of just Module since we need a clear way to wrap a Module as an Instance and I think Module.wrap(Module(new MyModule)) is just baffling.

@mwachs5
Copy link
Contributor

mwachs5 commented Aug 20, 2021

How is Reset type inference supposed to work:
https://scastie.scala-lang.org/0D1KuqS4SxO22uUtmJ8xiQ

if I am deep within some module of abstract reset type, if I build my definition there, i would want to indicate the reset type somehow. What is the right way to do that?

Copy link
Contributor Author

@azidar azidar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review

core/src/main/scala/chisel3/BlackBox.scala Show resolved Hide resolved
core/src/main/scala/chisel3/Module.scala Show resolved Hide resolved
core/src/main/scala/chisel3/Module.scala Show resolved Hide resolved
core/src/main/scala/chisel3/Module.scala Show resolved Hide resolved
@azidar azidar marked this pull request as ready for review September 2, 2021 15:59
@seldridge
Copy link
Member

On the record: this is the biggest single needed improvement to Chisel in it's ~10 year existence from a language perspective. I'm very excited to see this converging.

@jackkoenig
Copy link
Contributor

jackkoenig commented Sep 3, 2021

EDIT I tried to mark these as ignore but they don't compile sooooo

Because I don't want to merge commented tests, here are @azidar's Aspect tests that were commented out and are future work:

  describe("8: Aspect APIs work properly"){
    it("8.0: Work annotating via an aspect") {
      val aspect = aop.injecting.InjectingAspect(
        {adder: AddTwo => Seq(adder)},
        {adder: AddTwo =>
          mark(adder.i0.innerWire, "rel")
        }
      )
      val mt = "~AddTwo|AddTwo".mt
      val rt = "~AddTwo|AddTwo/i0:AddOne>innerWire".rt
      check(new AddTwo(), aspect, {
        case aop.injecting.InjectStatement(mt, _, _, Seq(MarkAnnotation(rt, "rel"))) => true
      })
    }
    it("8.1: Work injecting via an aspect into a definition") {
      val aspect = aop.injecting.InjectingAspect(
        {adder: AddTwo => Seq(adder.definition.module)},
        {add1: AddOne =>
          printf("In Add1")
        }
      )
      import _root_.firrtl.ir._
      import _root_.firrtl.Mappers._
      val mt = "~AddTwo|AddOne".mt
      check(new AddTwo(), aspect, {
        case aop.injecting.InjectStatement(mt, b: _root_.firrtl.ir.Block, Nil, Nil) =>
          println(b.stmts)
          def onStmt(s: Statement): Unit = {
            s.foreachStmt(onStmt)
          }
          b.stmts.collectFirst {
            case p: Print =>
            case p: Print =>
          }.getOrElse(false)
      })
    }
  }

Copy link
Member

@seldridge seldridge 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 awesome.

I reviewed the documentation and skimmed over the tests (these are extremely exhaustive). I did find the tests hard to follow due to the liberal use of the overloaded check method and wonder if these could be refactored to make each test more self-contained. I'm not sure that's worth fixing, though. (And it's something that can certainly happen once this lands if we want to do that.)

docs/src/cookbooks/hierarchy.md Outdated Show resolved Hide resolved
docs/src/cookbooks/hierarchy.md Outdated Show resolved Hide resolved
docs/src/cookbooks/hierarchy.md Outdated Show resolved Hide resolved
docs/src/cookbooks/hierarchy.md Outdated Show resolved Hide resolved
docs/src/cookbooks/hierarchy.md Outdated Show resolved Hide resolved
docs/src/cookbooks/hierarchy.md Outdated Show resolved Hide resolved
docs/src/cookbooks/hierarchy.md Outdated Show resolved Hide resolved

Thus, it is important that when converting normal modules to use this package, you are careful about what you mark as `IsLookupable`.

In the following example, we added the trait `IsLookupable` to allow the member to be marked `@public`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example could benefit from showing what you can do with this. E.g., accessing an instance of MyModule's public x.

This introduces a new experimental API for module instantiation that disentagles
elaborating the definition (or implementation) from instantiation of
a given module. This solves Chisel's longstanding reliance on
"Deduplication" for generating Verilog with multiple instances of the
same module.

The new API resides in package chisel3.experimental.hierarchy. Please
see the hierarchy ScalaDoc, documentation, and tests for examples of
use.

Co-authored-by: Jack Koenig <koenig@sifive.com>
Co-authored-by: Megan Wachs <megan@sifive.com>
Co-authored-by: Schuyler Eldridge <schuyler.eldridge@sifive.com>
@jackkoenig jackkoenig added this to the 3.5.0 milestone Sep 5, 2021
@jackkoenig jackkoenig added the Feature New feature, will be included in release notes label Sep 5, 2021
@jackkoenig jackkoenig merged commit 9fa8da2 into master Sep 5, 2021
@jackkoenig jackkoenig deleted the instance-definition branch September 5, 2021 19:11
@sequencer
Copy link
Member

This commit breaks dsptool compiling in https://github.com/sequencer/playground/runs/3519024613?check_suite_focus=true#step:9:3638
I'll fix that later this week.

poemonsense added a commit to OpenXiangShan/XiangShan that referenced this pull request Sep 26, 2021
This commit applys Definition and Instance for some modules. Refer to
chipsalliance/chisel#2045.
poemonsense added a commit to OpenXiangShan/XiangShan that referenced this pull request Sep 27, 2021
This commit applys Definition and Instance for some modules. Refer to
chipsalliance/chisel#2045.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature New feature, will be included in release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants