diff --git a/core/src/main/scala-2/chisel3/choice/DynamicGroupIntf.scala b/core/src/main/scala-2/chisel3/choice/DynamicGroupIntf.scala new file mode 100644 index 00000000000..9b76cc65127 --- /dev/null +++ b/core/src/main/scala-2/chisel3/choice/DynamicGroupIntf.scala @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chisel3.choice + +import scala.language.experimental.macros +import scala.reflect.macros.blackbox.Context + +private[chisel3] trait DynamicGroupFactoryIntf { + implicit def materializeDynamicGroupFactory[T <: DynamicGroup]: DynamicGroup.Factory[T] = + macro DynamicGroupMacros.materializeFactory[T] +} + +private[chisel3] object DynamicGroupMacros { + def materializeFactory[T <: DynamicGroup: c.WeakTypeTag](c: Context): c.Tree = { + import c.universe._ + + val dynamicGroupTpe = weakTypeOf[DynamicGroup] + val caseTpe = weakTypeOf[Case] + val sourceInfoTpe = weakTypeOf[chisel3.experimental.SourceInfo] + val targetTpe = weakTypeOf[T] + val targetSym = targetTpe.typeSymbol + + if (!targetSym.isClass || !targetSym.asClass.isTrait) { + c.abort( + c.enclosingPosition, + s"DynamicGroup can only be materialized for traits, got: ${targetTpe.typeSymbol.fullName}" + ) + } + if (!(targetTpe <:< dynamicGroupTpe)) { + c.abort(c.enclosingPosition, s"${targetTpe.typeSymbol.fullName} must extend chisel3.choice.DynamicGroup") + } + + val caseNames = targetTpe.decls.toList.collect { + case module: ModuleSymbol if module.typeSignature <:< caseTpe => + module.name.decodedName.toString.trim + }.reverse + + if (caseNames.isEmpty) { + c.abort(c.enclosingPosition, s"${targetSym.fullName} must declare at least one `object ... extends Case`") + } + + val caseNameTrees = caseNames.map(name => Literal(Constant(name))) + + q""" + new _root_.chisel3.choice.DynamicGroup.Factory[$targetTpe] { + override val caseNames: _root_.scala.Seq[String] = _root_.scala.Seq(..$caseNameTrees) + override def create()(implicit sourceInfo: $sourceInfoTpe): $targetTpe = + new $targetTpe {} + } + """ + } +} diff --git a/core/src/main/scala-3/chisel3/choice/DynamicGroupIntf.scala b/core/src/main/scala-3/chisel3/choice/DynamicGroupIntf.scala new file mode 100644 index 00000000000..ab27a65291e --- /dev/null +++ b/core/src/main/scala-3/chisel3/choice/DynamicGroupIntf.scala @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chisel3.choice + +private[chisel3] trait DynamicGroupFactoryIntf diff --git a/core/src/main/scala/chisel3/choice/package.scala b/core/src/main/scala/chisel3/choice/package.scala index 72c7672c75d..fe80ac0ca64 100644 --- a/core/src/main/scala/chisel3/choice/package.scala +++ b/core/src/main/scala/chisel3/choice/package.scala @@ -4,6 +4,7 @@ package chisel3 import chisel3.experimental.{BaseModule, SourceInfo} import chisel3.util.simpleClassName +import chisel3.internal.Builder /** This package contains Chisel language definitions for describing configuration options and their accepted values. */ @@ -30,6 +31,89 @@ package object choice { final implicit def group: Group = this } + /** Schema-style dynamic option group. + * + * This allows trait-based declarations of cases without requiring a singleton [[Group]] object. + * + * @example + * {{{ + * trait PlatformType extends DynamicGroup { + * object FPGA extends Case + * object ASIC extends Case + * } + * }}} + */ + trait DynamicGroup { + private var initializedName: Option[String] = None + private var initializedCaseNames: Option[Seq[String]] = None + private var initializedSourceInfo: Option[SourceInfo] = None + + private def groupName: String = initializedName.getOrElse( + throw new IllegalStateException("DynamicGroup was used before it was initialized") + ) + + private def caseNames: Seq[String] = initializedCaseNames.getOrElse( + throw new IllegalStateException("DynamicGroup was used before it was initialized") + ) + + private implicit def sourceInfo: SourceInfo = initializedSourceInfo.getOrElse( + throw new IllegalStateException("DynamicGroup was used before it was initialized") + ) + + private[chisel3] def initialize(name: String, caseNames: Seq[String], sourceInfo: SourceInfo): this.type = { + initializedName = Some(name) + initializedCaseNames = Some(caseNames.toVector) + initializedSourceInfo = Some(sourceInfo) + this + } + + private lazy val cachedGroup = Builder.getOrCreateDynamicGroup( + groupName, + caseNames.toVector, + DynamicGroup.groupFactory(groupName) + ) + + final implicit def group: Group = cachedGroup + } + object DynamicGroup { + private[chisel3] def groupFactory(groupName: String)(implicit sourceInfo: SourceInfo): () => Group = () => { + object DynamicGroupSingleton extends Group()(sourceInfo) { + override private[chisel3] def name = groupName + } + DynamicGroupSingleton + } + + /** Typeclass for constructing trait-based [[DynamicGroup]] instances. + */ + trait Factory[T <: DynamicGroup] { + def caseNames: Seq[String] + def create()(implicit sourceInfo: SourceInfo): T + } + object Factory extends DynamicGroupFactoryIntf + + /** Create a trait-based [[DynamicGroup]] using an implicit factory. + * + * @tparam T The trait type that defines the case structure + * @param groupName The name of the group + * @param sourceInfo Source location information + * @return An instance of T that provides type-safe access to the cases + * + * @example + * {{{ + * trait PlatformType extends DynamicGroup { + * object FPGA extends Case + * object ASIC extends Case + * } + * val platform = DynamicGroup[PlatformType]("Platform") + * }}} + */ + def apply[T <: DynamicGroup](groupName: String)(implicit factory: Factory[T], sourceInfo: SourceInfo): T = { + val instance = factory.create().initialize(groupName, factory.caseNames, sourceInfo) + instance.group + instance + } + } + /** An option case declaration. */ abstract class Case(implicit val group: Group, _sourceInfo: SourceInfo) { diff --git a/core/src/main/scala/chisel3/experimental/hierarchy/core/Definition.scala b/core/src/main/scala/chisel3/experimental/hierarchy/core/Definition.scala index a4d6de129c8..781870d62bd 100644 --- a/core/src/main/scala/chisel3/experimental/hierarchy/core/Definition.scala +++ b/core/src/main/scala/chisel3/experimental/hierarchy/core/Definition.scala @@ -131,7 +131,7 @@ object Definition extends SourceInfoDoc { Builder.components ++= ir._circuit.components Builder.annotations ++= ir._circuit.annotations Builder.layers ++= dynamicContext.layers - Builder.options ++= dynamicContext.options + Builder.addOptions(dynamicContext.options.values) dynamicContext.definitions.foreach(Builder.addDefinition) module._circuit = Builder.currentModule module.toDefinition diff --git a/core/src/main/scala/chisel3/internal/Builder.scala b/core/src/main/scala/chisel3/internal/Builder.scala index 3d27464bf4f..1bf6019da5e 100644 --- a/core/src/main/scala/chisel3/internal/Builder.scala +++ b/core/src/main/scala/chisel3/internal/Builder.scala @@ -533,8 +533,10 @@ private[chisel3] class DynamicContext( val components = ArrayBuffer[Component]() val annotations = ArrayBuffer[() => Seq[Annotation]]() val layers = mutable.LinkedHashSet[layer.Layer]() - val options = mutable.LinkedHashSet[choice.Case]() + val options = mutable.LinkedHashMap[(choice.Group, String), choice.Case]() val domains = mutable.LinkedHashSet[domain.Domain]() + val dynamicGroupsByName = mutable.HashMap[String, (choice.Group, Seq[String])]() + val dynamicCasesByGroupAndName = mutable.HashMap[(choice.Group, String), choice.Case]() var currentModule: Option[BaseModule] = None // Views that do not correspond to a single ReferenceTarget and thus require renaming @@ -611,8 +613,16 @@ private[chisel3] object Builder extends LazyLogging { def annotations: ArrayBuffer[() => Seq[Annotation]] = dynamicContext.annotations def layers: mutable.LinkedHashSet[layer.Layer] = dynamicContext.layers - def options: mutable.LinkedHashSet[choice.Case] = dynamicContext.options - def domains: mutable.LinkedHashSet[domain.Domain] = dynamicContext.domains + def options: mutable.LinkedHashMap[(choice.Group, String), choice.Case] = dynamicContext.options + def addOption(option: choice.Case): Unit = { + dynamicContext.options.getOrElseUpdate((option.group, option.name), option) + } + def addOptions(options: Iterable[choice.Case]): Unit = options.foreach(addOption) + def domains: mutable.LinkedHashSet[domain.Domain] = dynamicContext.domains + + def dynamicGroupsByName: mutable.HashMap[String, (choice.Group, Seq[String])] = dynamicContext.dynamicGroupsByName + def dynamicCasesByGroupAndName: mutable.HashMap[(choice.Group, String), choice.Case] = + dynamicContext.dynamicCasesByGroupAndName def contextCache: BuilderContextCache = dynamicContext.contextCache @@ -864,6 +874,31 @@ private[chisel3] object Builder extends LazyLogging { def elaborationTrace: ElaborationTrace = dynamicContext.elaborationTrace + def getOrCreateDynamicGroup(name: String, caseNames: Seq[String], groupFactory: () => choice.Group): choice.Group = { + if (!inContext) return groupFactory() + + dynamicGroupsByName.get(name) match { + case Some((existingGroup, existingCaseNames)) => + if (existingCaseNames != caseNames) { + throw new IllegalArgumentException( + s"DynamicGroup '$name' already exists with different case names.\n" + + s" Existing: ${existingCaseNames.mkString(", ")}\n" + + s" New: ${caseNames.mkString(", ")}" + ) + } + existingGroup + case None => + val newGroup = groupFactory() + dynamicGroupsByName(name) = (newGroup, caseNames) + newGroup + } + } + + def getOrCreateDynamicCase(group: choice.Group, name: String, caseFactory: () => choice.Case): choice.Case = { + if (!inContext) return caseFactory() + dynamicCasesByGroupAndName.getOrElseUpdate((group, name), caseFactory()) + } + def forcedClock: Clock = currentClock.getOrElse( // TODO add implicit clock change to Builder.exception throwException("Error: No implicit clock.") @@ -1109,7 +1144,7 @@ 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) => + val optionDefs = groupByIntoSeq(options.values)(opt => opt.group).map { case (optGroup, cases) => DefOption( optGroup.sourceInfo, optGroup.name, diff --git a/src/main/scala/chisel3/choice/ModuleChoice.scala b/src/main/scala/chisel3/choice/ModuleChoice.scala index 164fc602b04..9901d01502c 100644 --- a/src/main/scala/chisel3/choice/ModuleChoice.scala +++ b/src/main/scala/chisel3/choice/ModuleChoice.scala @@ -26,11 +26,11 @@ object ModuleChoice extends ModuleChoice$Intf { if (!instModule.io.typeEquivalent(instDefaultModule.io)) { Builder.error("Error: choice module IO bundles are not type equivalent") } - Builder.options += choice + Builder.addOption(choice) (choice, instModule) } - groupByIntoSeq(choiceModules.map(_._1))(opt => opt).foreach { case (_, group) => + groupByIntoSeq(choiceModules.map(_._1))(opt => (opt.group, opt.name)).foreach { case (_, group) => if (group.size != 1) { throw new IllegalArgumentException(s"Error: duplicate case '${group.head.name}'") } diff --git a/src/test/scala-2/chiselTests/DynamicGroupSpec.scala b/src/test/scala-2/chiselTests/DynamicGroupSpec.scala new file mode 100644 index 00000000000..ff142c33d1f --- /dev/null +++ b/src/test/scala-2/chiselTests/DynamicGroupSpec.scala @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chiselTests + +import chisel3._ +import chisel3.choice.{Case, DynamicGroup, Group, ModuleChoice} +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 { + + trait PlatformType extends DynamicGroup { + object FPGA extends Case + object ASIC extends Case + } + + trait PlatformGpuType extends DynamicGroup { + object FPGA extends Case + object GPU extends Case + } + + trait ReorderedPlatformType extends DynamicGroup { + object ASIC extends Case + object FPGA extends Case + } + + 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 { + val platform = DynamicGroup[PlatformType]("Platform") + + class ModuleWithDynamicChoice extends Module { + 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 "allow same DynamicGroup name to be reused" in { + class ModuleWithReusedGroup extends Module { + val platform1 = DynamicGroup[PlatformType]("Platform") + val platform2 = DynamicGroup[PlatformType]("Platform") // Should share the same group + + 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 + } + + ChiselStage + .emitCHIRRTL(new ModuleWithReusedGroup) + .fileCheck()( + """|CHECK: option Platform : + |CHECK-NEXT: FPGA + |CHECK-NEXT: ASIC + |CHECK-NOT: option Platform : + """.stripMargin + ) + + } + + it should "reject DynamicGroup with same name but different cases" in { + class ModuleWithMismatchedCases extends Module { + val platform1 = DynamicGroup[PlatformType]("Platform") + val platform2 = DynamicGroup[PlatformGpuType]("Platform") + } + + val exception = intercept[IllegalArgumentException] { + ChiselStage.emitCHIRRTL(new ModuleWithMismatchedCases) + } + + exception.getMessage should include("DynamicGroup 'Platform' already exists with different case names") + exception.getMessage should include("FPGA") + exception.getMessage should include("ASIC") + exception.getMessage should include("GPU") + } + + it should "reject DynamicGroup with same cases but different order" in { + class ModuleWithDifferentOrder extends Module { + val platform1 = DynamicGroup[PlatformType]("Platform") + val platform2 = DynamicGroup[ReorderedPlatformType]("Platform") + } + + val exception = intercept[IllegalArgumentException] { + ChiselStage.emitCHIRRTL(new ModuleWithDifferentOrder) + } + + exception.getMessage should include("DynamicGroup 'Platform' already exists with different case names") + exception.getMessage should include("FPGA") + exception.getMessage should include("ASIC") + } + + it should "work with trait-based API" in { + val platform = DynamicGroup[PlatformType]("Platform") + + class ModuleWithTraitAPI extends Module { + val inst = ModuleChoice(new VerifTarget)( + Seq( + platform.FPGA -> new FPGATarget, + platform.ASIC -> new ASICTarget + ) + ) + val io = IO(inst.cloneType) + io <> inst + } + + ChiselStage + .emitCHIRRTL(new ModuleWithTraitAPI) + .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 + ) + } +} diff --git a/src/test/scala-2/chiselTests/simulator/ModuleChoiceSimulationSpec.scala b/src/test/scala-2/chiselTests/simulator/ModuleChoiceSimulationSpec.scala index 2ab821bbf9c..6af738cd251 100644 --- a/src/test/scala-2/chiselTests/simulator/ModuleChoiceSimulationSpec.scala +++ b/src/test/scala-2/chiselTests/simulator/ModuleChoiceSimulationSpec.scala @@ -3,7 +3,7 @@ package chiselTests.simulator import chisel3._ -import chisel3.choice.{Case, Group, ModuleChoice} +import chisel3.choice.{Case, DynamicGroup, Group, ModuleChoice} import chisel3.simulator.{InstanceChoiceControl, Settings} import chisel3.simulator.InstanceChoiceControl.SpecializationTime import chisel3.simulator.scalatest.ChiselSim @@ -16,7 +16,7 @@ object Platform extends Group { object ASIC extends Case } -object Opt extends Group { +trait OptType extends DynamicGroup { object Fast extends Case } @@ -47,9 +47,11 @@ class ModuleChoiceTestModule extends Module { out1 := choiceOut1.out + // Use a dynamic group + val group = DynamicGroup[OptType]("Opt") val choiceOut2 = ModuleChoice(new Return0)( Seq( - Opt.Fast -> new Return1 + group.Fast -> new Return1 ) )