Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions core/src/main/scala/chisel3/choice/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,60 @@ package object choice {
final implicit def group: Group = this
}

/** Dynamic option group with runtime-customizable name.
*
* Unlike static [[Group]] objects, DynamicGroup allows the group name to be specified at instantiation time.
* This is useful for parameterized designs where the same group structure is reused with different names.
*
* @param customName The runtime name for this group
*
* @example
* {{{
* class Opt(name: String)(implicit sourceInfo: SourceInfo) extends DynamicGroup(name) {
* object Fast extends DynamicCase
* object Slow extends DynamicCase
* }
*
* // Use with ModuleChoice
* class MyModule extends Module {
* val opt = new Opt("OptMyModule")
* val impl = ModuleChoice(new DefaultImpl)(
* Seq(
* opt.Fast -> new FastImpl,
* opt.Slow -> new SlowImpl
* )
* )
* }
* }}}
*/
abstract class DynamicGroup(customName: String)(implicit _sourceInfo: SourceInfo) {
private[chisel3] def sourceInfo: SourceInfo = _sourceInfo

private[chisel3] val groupName: String = customName

private val _group: Group = {
object DynamicGroupSingleton extends Group()(sourceInfo) {
override private[chisel3] def name = groupName
}
DynamicGroupSingleton
}

final def group: Group = _group

// Provide an implicit DynamicGroup for DynamicCase objects
implicit protected def implicitGroup: DynamicGroup = this
}

/** An option case declaration for [[DynamicGroup]].
*
* DynamicCase objects must be defined inside a DynamicGroup class.
* They use implicit parameters to automatically associate with their parent DynamicGroup.
*/
abstract class DynamicCase(implicit val dynamicGroup: DynamicGroup, _sourceInfo: SourceInfo)
extends Case()(using dynamicGroup.group, _sourceInfo) {
self: Singleton =>
}

/** An option case declaration.
*/
abstract class Case(implicit val group: Group, _sourceInfo: SourceInfo) {
Expand Down
38 changes: 34 additions & 4 deletions core/src/main/scala/chisel3/internal/Builder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,22 @@ private[chisel3] object Builder extends LazyLogging {

def elaborationTrace: ElaborationTrace = dynamicContext.elaborationTrace

private def validateGroupCases(
groupName: String,
expectedCases: Seq[String],
actualCases: Seq[String],
context: String = ""
): Unit = {
if (expectedCases != actualCases) {
val contextMsg = if (context.nonEmpty) s" ($context)" else ""
throw new IllegalArgumentException(
s"Group '$groupName' has inconsistent case definitions$contextMsg.\n" +
s" Expected cases: ${expectedCases.mkString(", ")}\n" +
s" Found cases: ${actualCases.mkString(", ")}"
)
}
}

def forcedClock: Clock = currentClock.getOrElse(
// TODO add implicit clock change to Builder.exception
throwException("Error: No implicit clock.")
Expand Down Expand Up @@ -1110,11 +1126,25 @@ private[chisel3] object Builder extends LazyLogging {
Layer(l.sourceInfo, l.name, config, children.map(foldLayers).toSeq, l)
}

val optionDefs = groupByIntoSeq(options)(opt => opt.group).map { case (optGroup, cases) =>
// Group by name to handle multiple Group objects with the same name (e.g., multiple DynamicGroup instances)
val optionDefs = groupByIntoSeq(options)(opt => opt.group.name).map { case (groupName, cases) =>
val representativeGroup = cases.head.group
val allCaseNames = cases.map(_.name).distinct

// Validate all Group objects with same name have same cases
// Catches: static vs DynamicGroup conflicts, or DynamicGroups with same name but different cases
val distinctGroups = cases.map(_.group).distinct
if (distinctGroups.size > 1) {
distinctGroups.tail.foreach { otherGroup =>
val otherCases = cases.filter(_.group == otherGroup).map(_.name).distinct
validateGroupCases(groupName, allCaseNames, otherCases, "group conflict")
}
}

DefOption(
optGroup.sourceInfo,
optGroup.name,
cases.map(optCase => DefOptionCase(optCase.sourceInfo, optCase.name))
representativeGroup.sourceInfo,
groupName,
cases.distinctBy(_.name).map(optCase => DefOptionCase(optCase.sourceInfo, optCase.name))
)
}

Expand Down
175 changes: 175 additions & 0 deletions src/test/scala-2/chiselTests/DynamicGroupSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: Apache-2.0

package chiselTests

import chisel3._
import chisel3.choice.{Case, DynamicCase, DynamicGroup, Group, ModuleChoice}
import chisel3.experimental.SourceInfo
import chisel3.testing.scalatest.FileCheck
import circt.stage.ChiselStage
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class DynamicGroupSpec extends AnyFlatSpec with Matchers with FileCheck {

class PlatformType(customName: String)(implicit sourceInfo: SourceInfo) extends DynamicGroup(customName) {
object FPGA extends DynamicCase
object ASIC extends DynamicCase
}

class PlatformGpuType(customName: String)(implicit sourceInfo: SourceInfo) extends DynamicGroup(customName) {
object FPGA extends DynamicCase
object GPU extends DynamicCase
}

class ReorderedPlatformType(customName: String)(implicit sourceInfo: SourceInfo) extends DynamicGroup(customName) {
object ASIC extends DynamicCase
object FPGA extends DynamicCase
}

class TargetIO(width: Int) extends Bundle {
val in = Flipped(UInt(width.W))
val out = UInt(width.W)
}

class FPGATarget extends FixedIORawModule[TargetIO](new TargetIO(8)) {
io.out := io.in
}

class ASICTarget extends FixedIOExtModule[TargetIO](new TargetIO(8))

class VerifTarget extends FixedIORawModule[TargetIO](new TargetIO(8)) {
io.out := io.in
}

it should "emit options and cases with DynamicGroup" in {
class ModuleWithDynamicChoice extends Module {
val platform = new PlatformType("Platform")

val inst = ModuleChoice(new VerifTarget)(
Seq(
platform.FPGA -> new FPGATarget,
platform.ASIC -> new ASICTarget
)
)
val io = IO(inst.cloneType)
io <> inst
}

ChiselStage
.emitCHIRRTL(new ModuleWithDynamicChoice)
.fileCheck()(
"""|CHECK: option Platform :
|CHECK-NEXT: FPGA
|CHECK-NEXT: ASIC
|CHECK: instchoice inst of VerifTarget, Platform :
|CHECK-NEXT: FPGA => FPGATarget
|CHECK-NEXT: ASIC => ASICTarget""".stripMargin
)
}

it should "reject DynamicGroup with same name but different cases" in {
class ModuleWithMismatchedCases extends Module {
val platform1 = new PlatformType("Platform")
val platform2 = new PlatformGpuType("Platform")

val inst1 = ModuleChoice(new VerifTarget)(Seq(platform1.FPGA -> new FPGATarget))
val inst2 = ModuleChoice(new VerifTarget)(Seq(platform2.GPU -> new FPGATarget))
val io1 = IO(inst1.cloneType)
val io2 = IO(inst2.cloneType)
io1 <> inst1
io2 <> inst2
}

val exception = intercept[IllegalArgumentException] {
ChiselStage.emitCHIRRTL(new ModuleWithMismatchedCases)
}

exception.getMessage should include("Group 'Platform' has inconsistent case definitions")
}

it should "reject DynamicGroup with same cases but different order" in {
class ModuleWithDifferentOrder extends Module {
val platform1 = new PlatformType("Platform")
val platform2 = new ReorderedPlatformType("Platform")

val inst1 = ModuleChoice(new VerifTarget)(Seq(platform1.FPGA -> new FPGATarget))
val inst2 = ModuleChoice(new VerifTarget)(Seq(platform2.ASIC -> new ASICTarget))
val io1 = IO(inst1.cloneType)
val io2 = IO(inst2.cloneType)
io1 <> inst1
io2 <> inst2
}

val exception = intercept[IllegalArgumentException] {
ChiselStage.emitCHIRRTL(new ModuleWithDifferentOrder)
}

exception.getMessage should include("Group 'Platform' has inconsistent case definitions")
}

it should "allow same DynamicGroup name across different submodules" in {
class SubModule1 extends Module {
val platform = new PlatformType("Platform")
val inst = ModuleChoice(new VerifTarget)(Seq(platform.FPGA -> new FPGATarget, platform.ASIC -> new ASICTarget))
val io = IO(inst.cloneType)
io <> inst
}

class SubModule2 extends Module {
val platform = new PlatformType("Platform") // Same name, same cases - should work
val inst = ModuleChoice(new VerifTarget)(Seq(platform.FPGA -> new FPGATarget, platform.ASIC -> new ASICTarget))
val io = IO(inst.cloneType)
io <> inst
}

class TopModule extends Module {
val sub1 = Module(new SubModule1)
val sub2 = Module(new SubModule2)
val io1 = IO(sub1.io.cloneType)
val io2 = IO(sub2.io.cloneType)
io1 <> sub1.io
io2 <> sub2.io
}

ChiselStage
.emitCHIRRTL(new TopModule)
.fileCheck()(
"""|CHECK: option Platform :
|CHECK-NEXT: FPGA
|CHECK-NEXT: ASIC
|CHECK-NOT: option Platform :
|CHECK: module SubModule1 :
|CHECK: instchoice inst of {{VerifTarget[_0-9]*}}, Platform :
|CHECK: module SubModule2 :
|CHECK: instchoice inst of {{VerifTarget[_0-9]*}}, Platform :
""".stripMargin
)
}

// Define a static group for testing static vs dynamic conflict
object StaticPlatform extends Group {
object FPGA extends Case
object ASIC extends Case
}

it should "reject conflict between static Group and DynamicGroup with same name" in {
class ModuleWithStaticGroup extends Module {
val inst1 = ModuleChoice(new VerifTarget)(Seq(StaticPlatform.FPGA -> new FPGATarget))
val platform = new PlatformGpuType("StaticPlatform") // Same name as static group, different cases
val inst2 = ModuleChoice(new VerifTarget)(Seq(platform.GPU -> new FPGATarget))
val io1 = IO(inst1.cloneType)
val io2 = IO(inst2.cloneType)
io1 <> inst1
io2 <> inst2
}

val exception = intercept[IllegalArgumentException] {
ChiselStage.emitCHIRRTL(new ModuleWithStaticGroup)
}

exception.getMessage should include("StaticPlatform")
exception.getMessage should include("inconsistent case definitions")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
package chiselTests.simulator

import chisel3._
import chisel3.choice.{Case, Group, ModuleChoice}
import chisel3.choice.{Case, DynamicCase, DynamicGroup, Group, ModuleChoice}
import chisel3.experimental.SourceInfo
import chisel3.simulator.{InstanceChoiceControl, Settings}
import chisel3.simulator.InstanceChoiceControl.SpecializationTime
import chisel3.simulator.scalatest.ChiselSim
Expand All @@ -16,8 +17,8 @@ object Platform extends Group {
object ASIC extends Case
}

object Opt extends Group {
object Fast extends Case
class OptType(customName: String)(implicit sourceInfo: SourceInfo) extends DynamicGroup(customName) {
object Fast extends DynamicCase
}

class TargetIO extends Bundle {
Expand Down Expand Up @@ -47,9 +48,11 @@ class ModuleChoiceTestModule extends Module {

out1 := choiceOut1.out

// Use a dynamic group
val group = new OptType("Opt")
val choiceOut2 = ModuleChoice(new Return0)(
Seq(
Opt.Fast -> new Return1
group.Fast -> new Return1
)
)

Expand Down
Loading