Skip to content

Commit

Permalink
[plugin] Implement autoclonetype in the compiler plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
jackkoenig committed Mar 13, 2021
1 parent e80e9a3 commit 1494231
Show file tree
Hide file tree
Showing 11 changed files with 388 additions and 80 deletions.
56 changes: 42 additions & 14 deletions core/src/main/scala/chisel3/Aggregate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,13 @@ abstract class Bundle(implicit compileOptions: CompileOptions) extends Record {
case _ => None
}

/** Indicates if a concrete Bundle class was compiled using the compiler plugin
*
* Used for optimizing Chisel's performance and testing Chisel itself
* @note This should not be used in user code!
*/
protected def _usingPlugin: Boolean = false

// Memoize the outer instance for autoclonetype, especially where this is context-dependent
// (like the outer module or enclosing Bundles).
private var _outerInst: Option[Object] = None
Expand All @@ -808,32 +815,53 @@ abstract class Bundle(implicit compileOptions: CompileOptions) extends Record {
private val _containingModule: Option[BaseModule] = Builder.currentModule
private val _containingBundles: Seq[Bundle] = Builder.updateBundleStack(this)

override def cloneType : this.type = {
private def checkClone(clone: Bundle): Unit = {
for ((name, field) <- elements) {
if (clone.elements(name) eq field) {
throw new AutoClonetypeException(
s"Automatically cloned $clone has field '$name' aliased with base $this." +
" In the future, this will be solved automatically by the compiler plugin." +
" For now, ensure Chisel types used in the Bundle definition are passed through constructor arguments," +
" or wrapped in Input(...), Output(...), or Flipped(...) if appropriate." +
" As a last resort, you can override cloneType manually."
)
}
}

}

override def cloneType: this.type = {
val clone = _cloneTypeImpl.asInstanceOf[this.type]
checkClone(clone)
clone
}

/** Implementation of cloneType using runtime reflection. This should _never_ be overridden or called in user-code
*
* @note This is overridden by the compiler plugin (it is never called when using the plugin)
*/
protected def _cloneTypeImpl: Bundle = {
assert(Builder.allowReflectiveAutoCloneType, "reflective autoclonetype is disallowed, this should only happen in testing")
// This attempts to infer constructor and arguments to clone this Bundle subtype without
// requiring the user explicitly overriding cloneType.
import scala.language.existentials
import scala.reflect.runtime.universe._

val clazz = this.getClass

def autoClonetypeError(desc: String): Nothing = {
throw new AutoClonetypeException(s"Unable to automatically infer cloneType on $clazz: $desc")
}
def autoClonetypeError(desc: String): Nothing =
throw new AutoClonetypeException(
s"Unable to automatically infer cloneType on $clazz. " +
"cloneType is now implemented by the Chisel compiler plugin so please ensure you are using it in your build. " +
"If you cannot use the compiler plugin or you are using it and you still see this message, please file an issue and let us know. " +
s"For those not using the plugin, here is the 'runtime reflection' cloneType error message: $desc"
)

def validateClone(clone: Bundle, equivDiagnostic: String): Unit = {
if (!clone.typeEquivalent(this)) {
autoClonetypeError(s"Automatically cloned $clone not type-equivalent to base $this. " + equivDiagnostic)
}

for ((name, field) <- elements) {
if (clone.elements(name) eq field) {
autoClonetypeError(s"Automatically cloned $clone has field $name aliased with base $this." +
" In the future, this can be solved by wrapping the field in Field(...)," +
" see https://github.com/freechipsproject/chisel3/pull/909." +
" For now, ensure Chisel types used in the Bundle definition are passed through constructor arguments," +
" or wrapped in Input(...), Output(...), or Flipped(...) if appropriate.")
}
}
checkClone(clone)
}

val mirror = runtimeMirror(clazz.getClassLoader)
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/scala/chisel3/internal/Builder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ private[chisel3] class DynamicContext(val annotationSeq: AnnotationSeq) {
val components = ArrayBuffer[Component]()
val annotations = ArrayBuffer[ChiselAnnotation]()
var currentModule: Option[BaseModule] = None
// This is only used for testing, it can be removed if the plugin becomes mandatory
var allowReflectiveAutoCloneType = true

/** Contains a mapping from a elaborated module to their aspect
* Set by [[ModuleAspect]]
Expand Down Expand Up @@ -530,6 +532,12 @@ private[chisel3] object Builder extends LazyLogging {
dynamicContext.currentReset = newReset
}

// This should only be used for testing
def allowReflectiveAutoCloneType: Boolean = dynamicContext.allowReflectiveAutoCloneType
def allowReflectiveAutoCloneType_=(value: Boolean): Unit = {
dynamicContext.allowReflectiveAutoCloneType = value
}

def forcedClock: Clock = currentClock.getOrElse(
throwException("Error: No implicit clock.")
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: Apache-2.0

package chisel3.internal.plugin

import scala.collection.mutable
import scala.tools.nsc
import scala.tools.nsc.{Global, Phase}
import scala.tools.nsc.plugins.PluginComponent
import scala.tools.nsc.symtab.Flags
import scala.tools.nsc.transform.TypingTransformers

// TODO This component could also implement val elements in Bundles
private[plugin] class BundleComponent(val global: Global) extends PluginComponent with TypingTransformers {
import global._

val phaseName: String = "chiselbundlephase"
val runsAfter: List[String] = "typer" :: Nil
def newPhase(prev: Phase): Phase = new BundlePhase(prev)

private class BundlePhase(prev: Phase) extends StdPhase(prev) {
override def name: String = phaseName
def apply(unit: CompilationUnit): Unit = {
// This plugin doesn't work on Scala 2.11 nor Scala 3. Rather than complicate the sbt build flow,
// instead we just check the version and if its an early Scala version, the plugin does nothing
val scalaVersion = scala.util.Properties.versionNumberString.split('.')
if (scalaVersion(0).toInt == 2 && scalaVersion(1).toInt >= 12) {
unit.body = new MyTypingTransformer(unit).transform(unit.body)
}
}
}

private class MyTypingTransformer(unit: CompilationUnit) extends TypingTransformer(unit) {

def inferType(t: Tree): Type = localTyper.typed(t, nsc.Mode.TYPEmode).tpe

val bundleTpe = inferType(tq"chisel3.Bundle")
val dataTpe = inferType(tq"chisel3.Data")

// Not cached because it should only be run once per class (thus once per Type)
def isBundle(sym: Symbol): Boolean = sym.tpe <:< bundleTpe

val isDataCache = new mutable.HashMap[Type, Boolean]
// Cached because this is run on every argument to every Bundle
def isData(sym: Symbol): Boolean = isDataCache.getOrElseUpdate(sym.tpe, sym.tpe <:< dataTpe)

def cloneTypeFull(tree: Tree): Tree =
localTyper.typed(q"chisel3.experimental.DataMirror.internal.chiselTypeClone[${tree.tpe}]($tree)")

def isNullaryMethodNamed(name: String, defdef: DefDef): Boolean =
defdef.name.decodedName.toString == name && defdef.tparams.isEmpty && defdef.vparamss.isEmpty

def getConstructorAndParams(body: List[Tree]): (Option[DefDef], Seq[Symbol]) = {
val paramAccessors = mutable.ListBuffer[Symbol]()
var primaryConstructor: Option[DefDef] = None
body.foreach {
case acc: ValDef if acc.symbol.isParamAccessor =>
paramAccessors += acc.symbol
case con: DefDef if con.symbol.isPrimaryConstructor =>
primaryConstructor = Some(con)
case d: DefDef if isNullaryMethodNamed("_cloneTypeImpl", d) =>
val msg = "Users cannot override _cloneTypeImpl. Let the compiler plugin generate it. If you must, override cloneType instead."
global.globalError(d.pos, msg)
case d: DefDef if isNullaryMethodNamed("_usingPlugin", d) =>
val msg = "Users cannot override _usingPlugin, it is for the compiler plugin's use only."
global.globalError(d.pos, msg)
case _ =>
}
(primaryConstructor, paramAccessors.toList)
}


override def transform(tree: Tree): Tree = tree match {

case bundle: ClassDef if isBundle(bundle.symbol) && !bundle.mods.hasFlag(Flag.ABSTRACT) =>

// ==================== Generate _cloneTypeImpl ====================
val (con, params) = getConstructorAndParams(bundle.impl.body)
if (con.isEmpty) {
global.reporter.warning(bundle.pos, "Unable to determine primary constructor!")
return super.transform(tree)
}
val constructor = con.get

val thiz = gen.mkAttributedThis(bundle.symbol)

// The params have spaces after them (Scalac implementation detail)
val paramLookup: String => Symbol = params.map(sym => sym.name.toString.trim -> sym).toMap

// Create a this.<ref> for each field matching order of constructor arguments
// List of Lists because we can have multiple parameter lists
val conArgs: List[List[Tree]] =
constructor.vparamss.map(_.map { vp =>
val p = paramLookup(vp.name.toString)
// Make this.<ref>
val select = gen.mkAttributedSelect(thiz, p)
// Clone any Data parameters to avoid field aliasing, need full clone to include direction
if (isData(vp.symbol)) cloneTypeFull(select) else select
})

val ttpe = Ident(bundle.symbol)
val neww = localTyper.typed(New(ttpe, conArgs))

// Create the symbol for the method and have it be associated with the Bundle class
val cloneTypeSym = bundle.symbol.newMethod(TermName("_cloneTypeImpl"), bundle.symbol.pos.focus, Flag.OVERRIDE | Flag.PROTECTED)
// Handwritten cloneTypes don't have the Method flag set, unclear if it matters
cloneTypeSym.resetFlag(Flags.METHOD)
// Need to set the type to chisel3.Bundle for the override to work
cloneTypeSym.setInfo(NullaryMethodType(bundleTpe))

val cloneTypeImpl = localTyper.typed(DefDef(cloneTypeSym, neww))

// ==================== Generate _usingPlugin ====================
// Unclear why quasiquotes work here but didn't for cloneTypeSym, maybe they could.
val usingPlugin = localTyper.typed(q"override protected def _usingPlugin: Boolean = true")

val withMethods = deriveClassDef(bundle) { t =>
deriveTemplate(t)(_ :+ cloneTypeImpl :+ usingPlugin)
}

super.transform(localTyper.typed(withMethods))

case _ => super.transform(tree)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ class ChiselComponent(val global: Global) extends PluginComponent with TypingTra
class ChiselComponentPhase(prev: Phase) extends StdPhase(prev) {
override def name: String = phaseName
def apply(unit: CompilationUnit): Unit = {
// This plugin doesn't work on Scala 2.11. Rather than complicate the sbt build flow,
// This plugin doesn't work on Scala 2.11 nor Scala 3. Rather than complicate the sbt build flow,
// instead we just check the version and if its an early Scala version, the plugin does nothing
if(scala.util.Properties.versionNumberString.split('.')(1).toInt >= 12) {
val scalaVersion = scala.util.Properties.versionNumberString.split('.')
if (scalaVersion(0).toInt == 2 && scalaVersion(1).toInt >= 12) {
unit.body = new MyTypingTransformer(unit).transform(unit.body)
}
}
Expand Down Expand Up @@ -211,4 +212,4 @@ class ChiselComponent(val global: Global) extends PluginComponent with TypingTra
case _ => super.transform(tree)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import nsc.plugins.{Plugin, PluginComponent}
class ChiselPlugin(val global: Global) extends Plugin {
val name = "chiselplugin"
val description = "Plugin for Chisel 3 Hardware Description Language"
val components: List[PluginComponent] = List[PluginComponent](new ChiselComponent(global))
val components: List[PluginComponent] = List[PluginComponent](
new ChiselComponent(global),
new BundleComponent(global)
)
}


12 changes: 12 additions & 0 deletions src/test/scala/chisel3/testers/TestUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,22 @@
package chisel3.testers

import TesterDriver.Backend
import chisel3.{Bundle, RawModule}
import chisel3.internal.firrtl.Circuit
import chisel3.stage.ChiselStage
import firrtl.AnnotationSeq

object TestUtils {
// Useful because TesterDriver.Backend is chisel3 package private
def containsBackend(annos: AnnotationSeq): Boolean =
annos.collectFirst { case b: Backend => b }.isDefined

// Allows us to check that the compiler plugin cloneType is actually working
val usingPlugin: Boolean = (new Bundle { def check = _usingPlugin }).check
def elaborateNoReflectiveAutoCloneType(f: => RawModule): Circuit = {
ChiselStage.elaborate {
chisel3.internal.Builder.allowReflectiveAutoCloneType = !usingPlugin
f
}
}
}
Loading

0 comments on commit 1494231

Please sign in to comment.