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

[core] Add Placeholder API #4628

Merged
merged 2 commits into from
Jan 24, 2025
Merged

[core] Add Placeholder API #4628

merged 2 commits into from
Jan 24, 2025

Conversation

seldridge
Copy link
Member

@seldridge seldridge commented Jan 16, 2025

Add an advanced Chisel API which allows for generation of hardware at a remembered point in the design. This API allows a user to create an object of class Placeholder which, if used later via its append method, will create hardware at the location where the Placeholder object was created.

This is intended to be used to support certain generator patterns where hardware must be created before some other hardware, but only after the other hardware has been built. E.g., this can be used to support creating and defining layer-colored probes after constructing a layer block.

@seldridge seldridge added the Feature New feature, will be included in release notes label Jan 16, 2025
@seldridge seldridge force-pushed the dev/seldridge/placeholder-api branch from be01c04 to 5d982aa Compare January 21, 2025 23:09
@seldridge seldridge added the No Release Notes Exclude from release notes, consider using Internal instead label Jan 22, 2025
core/src/main/scala/chisel3/internal/firrtl/IR.scala Outdated Show resolved Hide resolved
* wire a : UInt<1>
* }}}
*/
private[chisel3] class Placeholder()(implicit sourceInfo: SourceInfo) {
Copy link
Member

Choose a reason for hiding this comment

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

Should this document that footgun this enables regarding adding uses of hardware before they exist?

I know it says "advanced" but maybe better to be explicit....?

EDIT: There are many footguns possible here (see below), not sure reasonable to document neatly.

* @return the return value of the `thunk`
*/
def append[A](thunk: => A): A = {
Builder.currentBlock.get.appendToPlaceholder(placeholder)(thunk)
Copy link
Member

Choose a reason for hiding this comment

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

Did you mean currentBlock? Probably the expected behavior is that hardware generated in the placeholder belongs to the block containing the placeholder, not the current block we're appending from...?
(offhand not sure where to grab the right block, or I'd offer a concrete suggestion here)

Also, things like layerStack and whenStack will be wrong when evaluating thunk, probably other context bits maintained during top-down evaluation and not captured in the Command list s.t. you can jump around and insert this way.

For the when/layer stacks, these ideally would be replaced with walking up blocks to their parents (this was discussed/planned but postponed for reasons I don't have paged in). Details like elideLayerBlocks are not captured, just tracked as evaluation context. I haven't audited them all to separate those that maybe make sense as context regardless of insertion point, but offhand probably none of them (unless they'd always the same regardless...)?

That is, we're still mostly reliant on "context" state.

To help with the stacks, could require this is invoked from same block as that containing the placeholder -- but that doesn't address the other context details.

Basically, we don't have the infrastructure to do this safely.

What we have barely allows us to insert "secret connections".

Since it's now "private" (thanks, I suggested this as well...) and is probably fine for the use case envisioned for it, if we want to proceed anyway I won't stand in the way. I'd prefer we sorted this all out instead.

Copy link
Member

Choose a reason for hiding this comment

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

Consider this test you can add to PlaceholderSpec.scala:

  they should "do something fun when used across modules" in {

    object A extends layer.Layer(layer.LayerConfig.Extract())

    class Bar extends RawModule {
      val placeholder = new Placeholder()
    }
    class Foo extends RawModule {
      val in = IO(Input(Bool()))

      val bar = Module(new Bar)
      val x = bar.placeholder.append({ val w = Wire(UInt(1.W)); w := 0.U; w })
      println(x)
      x := 1.U
    }

    println(circt.stage.ChiselStage.emitCHIRRTL(new Foo))
    generateFirrtlAndFileCheck(new Foo) {
      s"""|CHECK:      wire
          |CHECK-NEXT: asdf
          |""".stripMargin
    }

  }

Which generates a connection to x from a different module. This is because of the issue mentioned above where the wrong block is used.

Output from print:

[372] circuit Foo :
[372]   layer Verification, bind, "verification" :
[372]     layer Assert, bind, "verification/assert" :
[372]     layer Assume, bind, "verification/assume" :
[372]     layer Cover, bind, "verification/cover" :
[372]   module Bar : @[src/test/scala/chisel3/PlaceholderSpec.scala 108:11]
[372]
[372]     wire x : UInt<1> @[src/test/scala/chisel3/PlaceholderSpec.scala 115:52]
[372]     connect x, UInt<1>(0h0) @[src/test/scala/chisel3/PlaceholderSpec.scala 115:67]
[372]
[372]   public module Foo : @[src/test/scala/chisel3/PlaceholderSpec.scala 111:11]
[372]     input in : UInt<1> @[src/test/scala/chisel3/PlaceholderSpec.scala 112:18]
[372]
[372]     inst bar of Bar @[src/test/scala/chisel3/PlaceholderSpec.scala 114:23]
[372]     connect x, UInt<1>(0h1) @[src/test/scala/chisel3/PlaceholderSpec.scala 117:9]

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this is a problem. Here's what's changed to address this:

  1. 4dd8f19 adds the ability to save and restore the Builder's state. This is a bit of a misnomer as it is really "state related to blocks, modules, layers, and whens", i.e., enough to do things like change the append point. You don't want to actually save the entire builder state as this includes global things.
  2. The Placeholder.append method is updated to save/change/restore the state using a method called guard that takes a thunk. This should then make this approach safe.

I added two tests that show that this does actually work (allows you to construct hardware in the parent) and will error when used improperly (disallows construction of hardware in a child, i.e., a closed module). I do think that it is reasonable to relax the closed module restriction (as this is essentially a pathway to enable a cleaner BoringUtils as well as a Chisel-native provider API).

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks reasonable enough to me, my approval remains

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm looking at this specific code again. I may merge this and then loop back to change it.

The problem right now is that the Builder expect that it is always working with a Block when, after this patch, it could be inserting into a Block or a Placeholder. I think we probably want this to revert to always pointing at a command builder directly.

core/src/main/scala/chisel3/internal/firrtl/IR.scala Outdated Show resolved Hide resolved
@@ -357,6 +378,14 @@ private[chisel3] object ir {
_commandsBuilder += c
}

def appendToPlaceholder[A](placeholder: Placeholder)(thunk: => A): A = {
val oldPoint = _commandsBuilder
Copy link
Member

Choose a reason for hiding this comment

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

This looks a little tricksy, but is probably not likely to break presently?

I'm worried about interaction with this and use of withRegion where we might insert into a placeholder (because we've mutated the block to point to its buffer) instead of the intended normal block buffer. Or anything that closes the block, so on.

@seldridge seldridge removed the Feature New feature, will be included in release notes label Jan 22, 2025
Comment on lines 345 to 371
def getBuffer: mutable.Builder[Command, ArraySeq[Command]] = {
if (buffer == null)
buffer = ArraySeq.newBuilder[Command]
buffer
}

}

object Placeholder {
def unapply(placeholder: Placeholder): Option[(SourceInfo, Seq[Command])] = {
Some(
(placeholder.sourceInfo, placeholder.getBuffer.result())
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to check that we don't accidentally append to the buffer after calling result?
We probably also need to memoize the result since: After calling result() the behavior of a Builder is undefined. No further methods should be called. [1]. It's not clear that you can even call result() multiple times.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is beefed up to work similarly to Block in 1bc48a5. Thanks for the pointer to the docs. It's somewhat surprising that this is just undefined as opposed to being "read once" as I had thought.

Copy link
Contributor

@jackkoenig jackkoenig left a comment

Choose a reason for hiding this comment

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

Please see comment about potentially protecting the builder better, but otherwise I think this is reasonable for making forward progress. I agree with Will about the longer term direction.

@seldridge seldridge force-pushed the dev/seldridge/placeholder-api branch from f8a6f45 to 6cf1d77 Compare January 23, 2025 01:14
Comment on lines +1239 to +1279
def guard[A](state: State)(thunk: => A): A = {
val old = save
restore(state)
val result = thunk
restore(old)
result
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Can/Should this be used in other places? Like Module.apply?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, probably. This is also highlighting that there is more that should be saved here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Much more is now saved with: 50805af

A follow-on PR showing this being used for Module.apply is here: #4641

I expect that this can also be used to replace the related Definition.apply. I haven't looked into this, yet.

@seldridge seldridge force-pushed the dev/seldridge/placeholder-api branch from 6cf1d77 to a73d2b2 Compare January 23, 2025 22:50
Add the ability to save and restore the Builder state in a sane way.  This
is intended for advanced generator APIs that want to create hardware in
another location and need to change the Builder state to something else
temporarily.

Signed-off-by: Schuyler Eldridge <[email protected]>
Add an advanced Chisel API which allows for generation of hardware at a
remembered point in the design.  This API allows a user to create an
object of `class Placeholder` which, if used later via its `append`
method, will create hardware at the location where the `Placeholder`
object was created.

This is intended to be used to support certain generator patterns where
hardware must be created _before_ some other hardware, but only after the
other hardware has been built.  E.g., this can be used to support creating
and defining layer-colored probes after constructing a layer block.

Signed-off-by: Schuyler Eldridge <[email protected]>
@seldridge seldridge force-pushed the dev/seldridge/placeholder-api branch from 897c742 to a018a54 Compare January 24, 2025 15:47
@seldridge seldridge merged commit a018a54 into main Jan 24, 2025
15 checks passed
@seldridge seldridge deleted the dev/seldridge/placeholder-api branch January 24, 2025 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
No Release Notes Exclude from release notes, consider using Internal instead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants