-
Notifications
You must be signed in to change notification settings - Fork 613
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package chisel3 | ||
|
||
import chisel3.experimental.SourceInfo | ||
import chisel3.internal.Builder | ||
import chisel3.internal.firrtl.ir | ||
|
||
/** A [[Placeholder]] is an _advanced_ API for Chisel generators to generate | ||
* additional hardware at a different point in the design. | ||
* | ||
* For example, this can be used to add a wire _before_ another wire: | ||
* {{{ | ||
* val placeholder = new Placeholder() | ||
* val a = Wire(Bool()) | ||
* val b = placeholder.append { | ||
* Wire(Bool()) | ||
* } | ||
* }}} | ||
* | ||
* This will generate the following FIRRTL where `b` is declared _before_ `a`: | ||
* {{{ | ||
* wire b : UInt<1> | ||
* wire a : UInt<1> | ||
* }}} | ||
*/ | ||
private[chisel3] class Placeholder()(implicit sourceInfo: SourceInfo) { | ||
|
||
private val state = Builder.State.save | ||
|
||
private val placeholder = Builder.pushCommand(new ir.Placeholder(sourceInfo = sourceInfo)) | ||
|
||
/** Generate the hardware of the `thunk` and append the result at the point | ||
* where this [[Placeholder]] was created. The return value will be what the | ||
* `thunk` returns which enables getting a reference to the generated | ||
* hardware. | ||
* | ||
* @param thunk the hardware to generate and append to the [[Placeholder]] | ||
* @return the return value of the `thunk` | ||
*/ | ||
def append[A](thunk: => A): A = Builder.State.guard(state) { | ||
Builder.currentBlock.get.appendToPlaceholder(placeholder)(thunk) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you mean Also, things like layerStack and whenStack will be wrong when evaluating 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 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider this test you can add to
Which generates a connection to Output from print:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks reasonable enough to me, my approval remains There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1199,6 +1199,85 @@ private[chisel3] object Builder extends LazyLogging { | |
} | ||
|
||
initializeSingletons() | ||
|
||
/** The representation of the state of the [[Builder]] at a current point in | ||
* time. This is intended to capture _enough_ information to insert hardware | ||
* at another point in the circuit. | ||
* | ||
* @see [[chisel3.Placeholder]] | ||
*/ | ||
case class State( | ||
currentModule: Option[BaseModule], | ||
whenStack: List[WhenContext], | ||
blockStack: List[Block], | ||
layerStack: List[layer.Layer], | ||
prefix: Prefix, | ||
clock: Option[Delayed[Clock]], | ||
reset: Option[Delayed[Reset]], | ||
enabledLayers: List[layer.Layer] | ||
) | ||
|
||
object State { | ||
|
||
/** Return a [[State]] suitable for doing module construction. | ||
*/ | ||
def default: State = State( | ||
currentModule = Builder.currentModule, | ||
whenStack = Nil, | ||
blockStack = Builder.blockStack, | ||
layerStack = layer.Layer.Root :: Nil, | ||
prefix = Nil, | ||
clock = None, | ||
reset = None, | ||
enabledLayers = Nil | ||
) | ||
|
||
/** Capture the current [[Builder]] state. | ||
*/ | ||
def save: State = { | ||
State( | ||
currentModule = Builder.currentModule, | ||
whenStack = Builder.whenStack, | ||
blockStack = Builder.blockStack, | ||
layerStack = Builder.layerStack, | ||
prefix = Builder.getPrefix, | ||
clock = Builder.currentClockDelayed, | ||
reset = Builder.currentResetDelayed, | ||
enabledLayers = Builder.enabledLayers.toList | ||
) | ||
} | ||
|
||
/** Set the [[Builder]] state to that provided by the parameter. | ||
* | ||
* @param state the state to set the [[Builder]] to | ||
*/ | ||
def restore(state: State) = { | ||
Builder.currentModule = state.currentModule | ||
Builder.whenStack = state.whenStack | ||
Builder.blockStack = state.blockStack | ||
Builder.layerStack = state.layerStack | ||
Builder.setPrefix(state.prefix) | ||
Builder.currentClock = state.clock | ||
Builder.currentReset = state.reset | ||
Builder.enabledLayers = enabledLayers | ||
} | ||
|
||
/** Run the `thunk` with the context provided by `state`. Save the [[Builder]] | ||
* state before the thunk and restore it afterwards. | ||
* | ||
* @param state change the [[Builder]] to this state when running the thunk | ||
* @param thunk some hardware to generate | ||
* @return whatever the `thunk` returns | ||
*/ | ||
def guard[A](state: State)(thunk: => A): A = { | ||
val old = save | ||
restore(state) | ||
val result = thunk | ||
restore(old) | ||
result | ||
} | ||
|
||
Comment on lines
+1272
to
+1279
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can/Should this be used in other places? Like There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
} | ||
|
||
/** Allows public access to the naming stack in Builder / DynamicContext, and handles invocations | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -337,6 +337,39 @@ private[chisel3] object ir { | |
) extends Definition | ||
case class DefObject(sourceInfo: SourceInfo, id: HasId, className: String) extends Definition | ||
|
||
class Placeholder(val sourceInfo: SourceInfo) extends Command { | ||
private var commandBuilder: mutable.Builder[Command, ArraySeq[Command]] = null | ||
|
||
private var commands: Seq[Command] = null | ||
|
||
private var closed: Boolean = false | ||
|
||
private def close(): Unit = { | ||
if (commandBuilder == null) | ||
commands = Seq.empty | ||
else | ||
commands = commandBuilder.result() | ||
closed = true | ||
} | ||
|
||
def getBuffer: mutable.Builder[Command, ArraySeq[Command]] = { | ||
require(!closed, "cannot add more commands to a closed Placeholder") | ||
if (commandBuilder == null) | ||
commandBuilder = ArraySeq.newBuilder[Command] | ||
commandBuilder | ||
} | ||
|
||
} | ||
|
||
object Placeholder { | ||
def unapply(placeholder: Placeholder): Option[(SourceInfo, Seq[Command])] = { | ||
placeholder.close() | ||
Some( | ||
(placeholder.sourceInfo, placeholder.commands) | ||
) | ||
} | ||
} | ||
|
||
class Block(val sourceInfo: SourceInfo) { | ||
// While building block, commands go into _commandsBuilder. | ||
private var _commandsBuilder = ArraySeq.newBuilder[Command] | ||
|
@@ -355,6 +388,14 @@ private[chisel3] object ir { | |
_commandsBuilder += c | ||
} | ||
|
||
def appendToPlaceholder[A](placeholder: Placeholder)(thunk: => A): A = { | ||
val oldPoint = _commandsBuilder | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
_commandsBuilder = placeholder.getBuffer | ||
val result = thunk | ||
_commandsBuilder = oldPoint | ||
result | ||
} | ||
|
||
def close() = { | ||
if (_commands == null) { | ||
_commands = _commandsBuilder.result() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package chisel3 | ||
|
||
import chiselTests.{ChiselFlatSpec, FileCheck} | ||
import circt.stage.ChiselStage | ||
|
||
class PlaceholderSpec extends ChiselFlatSpec with FileCheck { | ||
|
||
"Placeholders" should "allow insertion of commands" in { | ||
|
||
class Foo extends RawModule { | ||
|
||
val placeholder = new Placeholder() | ||
|
||
val a = Wire(UInt(1.W)) | ||
|
||
val b = placeholder.append { | ||
Wire(UInt(2.W)) | ||
} | ||
|
||
} | ||
|
||
generateFirrtlAndFileCheck(new Foo) { | ||
s"""|CHECK: wire b : UInt<2> | ||
|CHECK-NEXT: wire a : UInt<1> | ||
|""".stripMargin | ||
} | ||
|
||
} | ||
|
||
they should "be capable of being nested" in { | ||
|
||
class Foo extends RawModule { | ||
|
||
val placeholder_1 = new Placeholder() | ||
val placeholder_2 = placeholder_1.append(new Placeholder()) | ||
|
||
val a = Wire(UInt(1.W)) | ||
|
||
val b = placeholder_1.append { | ||
Wire(UInt(2.W)) | ||
} | ||
|
||
val c = placeholder_2.append { | ||
Wire(UInt(3.W)) | ||
} | ||
|
||
val d = placeholder_1.append { | ||
Wire(UInt(4.W)) | ||
} | ||
|
||
} | ||
|
||
generateFirrtlAndFileCheck(new Foo) { | ||
s"""|CHECK: wire c : UInt<3> | ||
|CHECK-NEXT: wire b : UInt<2> | ||
|CHECK-NEXT: wire d : UInt<4> | ||
|CHECK-NEXT: wire a : UInt<1> | ||
|""".stripMargin | ||
} | ||
|
||
} | ||
|
||
they should "emit no statements if empty" in { | ||
|
||
class Foo extends RawModule { | ||
val a = new Placeholder() | ||
} | ||
|
||
generateFirrtlAndFileCheck(new Foo) { | ||
"""|CHECK: public module Foo : | ||
|CHECK: skip | ||
|""".stripMargin | ||
} | ||
} | ||
|
||
they should "allow constructing hardware in a parent" in { | ||
|
||
class Bar(placeholder: Placeholder, a: Bool) extends RawModule { | ||
|
||
placeholder.append { | ||
val b = Wire(Bool()) | ||
b :<= a | ||
} | ||
|
||
} | ||
|
||
class Foo extends RawModule { | ||
|
||
val a = Wire(Bool()) | ||
val placeholder = new Placeholder | ||
|
||
val bar = Module(new Bar(placeholder, a)) | ||
|
||
} | ||
|
||
generateFirrtlAndFileCheck(new Foo) { | ||
"""|CHECK: module Bar : | ||
|CHECK-NOT: {{wire|connect}} | ||
| | ||
|CHECK: module Foo : | ||
|CHECK: wire a : UInt<1> | ||
|CHECK-NEXT: wire b : UInt<1> | ||
|CHECK-NEXT: connect b, a | ||
|CHECK-NEXT: inst bar of Bar | ||
|""".stripMargin | ||
} | ||
|
||
} | ||
|
||
// TODO: This test can be changed to pass in the future in support of advanced | ||
// APIs like Providers or an improved BoringUtils. | ||
they should "error if constructing hardware in a child" in { | ||
|
||
class Bar extends RawModule { | ||
|
||
val a = Wire(Bool()) | ||
val placeholder = new Placeholder() | ||
|
||
} | ||
|
||
class Foo extends RawModule { | ||
|
||
val bar = Module(new Bar) | ||
|
||
bar.placeholder.append { | ||
val b = Wire(Bool()) | ||
b :<= bar.a | ||
} | ||
|
||
} | ||
|
||
intercept[IllegalArgumentException] { ChiselStage.emitCHIRRTL(new Foo) }.getMessage() should include( | ||
"Can't write to module after module close" | ||
) | ||
|
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package chisel3 | ||
|
||
import chisel3.aop.Select | ||
import chisel3.stage.{ChiselGeneratorAnnotation, DesignAnnotation} | ||
import chiselTests.ChiselFlatSpec | ||
|
||
class SelectSpec extends ChiselFlatSpec { | ||
|
||
"Placeholders" should "be examined" in { | ||
class Foo extends RawModule { | ||
val placeholder = new Placeholder() | ||
val a = Wire(Bool()) | ||
val b = placeholder.append { | ||
Wire(Bool()) | ||
} | ||
} | ||
val design = ChiselGeneratorAnnotation(() => { | ||
new Foo | ||
}).elaborate(1).asInstanceOf[DesignAnnotation[Foo]].design | ||
Select.wires(design).size should be(2) | ||
} | ||
|
||
} |
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.
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.