From 7c3c18de2ffd56af51b99030c7ae7d3a321aed5f Mon Sep 17 00:00:00 2001 From: ducky Date: Fri, 3 Nov 2017 18:34:46 -0700 Subject: Autoclonetype initial prototype --- .../src/main/scala/chisel3/core/Aggregate.scala | 123 +++++++++++++++++---- .../src/main/scala/chisel3/core/Binding.scala | 2 +- .../src/main/scala/chisel3/core/BlackBox.scala | 8 +- .../src/main/scala/chisel3/core/Module.scala | 62 ++++++----- .../src/main/scala/chisel3/core/UserModule.scala | 6 +- src/main/scala/chisel3/compatibility.scala | 12 +- src/main/scala/chisel3/testers/BasicTester.scala | 9 +- src/test/scala/chiselTests/AutoClonetypeSpec.scala | 34 ++++++ 8 files changed, 190 insertions(+), 66 deletions(-) create mode 100644 src/test/scala/chiselTests/AutoClonetypeSpec.scala diff --git a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala index b89961aa..d4d67018 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala @@ -557,30 +557,107 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { } override def cloneType : this.type = { - // If the user did not provide a cloneType method, try invoking one of - // the following constructors, not all of which necessarily exist: - // - A zero-parameter constructor - // - A one-paramater constructor, with null as the argument - // - A one-parameter constructor for a nested Bundle, with the enclosing - // parent Module as the argument - val constructor = this.getClass.getConstructors.head - try { - val args = Seq.fill(constructor.getParameterTypes.size)(null) - constructor.newInstance(args:_*).asInstanceOf[this.type] - } catch { - case e: java.lang.reflect.InvocationTargetException if e.getCause.isInstanceOf[java.lang.NullPointerException] => - try { - constructor.newInstance(_parent.get).asInstanceOf[this.type] - } catch { - case _: java.lang.reflect.InvocationTargetException | _: java.lang.IllegalArgumentException => - Builder.exception(s"Parameterized Bundle ${this.getClass} needs cloneType method. You are probably using " + - "an anonymous Bundle object that captures external state and hence is un-cloneTypeable") - this - } - case _: java.lang.reflect.InvocationTargetException | _: java.lang.IllegalArgumentException => - Builder.exception(s"Parameterized Bundle ${this.getClass} needs cloneType method") - this + // Automatic Bundle cloning is inferred as follows: + // - User-overloaded cloneType is called instead of this (if supplied, so this method is never called) + // - Attempt to automatically fill in constructor parameters by using Scala reflection: + // - If multiple constructors, error out + // - If the constructor argument list can be matched to param accessors, call the constructor with + // the result of those accessors + // - Otherwise, error out + // - If Scala reflection crashes, fall back to Java reflection and inspect the constructor argument list: + // - If no parameters, invoke constructor directly. That was easy! + // - If the first parameter's type is equal to this Module, assume the Bundle is an inner class + // and invoke it with the current containing Module + // - Error out to the user + + def reflectError(desc: String) { + Builder.exception(s"Unable to automatically infer cloneType on $this: $desc") + } + + // Try Scala reflection + import scala.reflect.runtime.universe._ + + val mirror = runtimeMirror(this.getClass.getClassLoader) + val decls = mirror.classSymbol(this.getClass).typeSignature.decls + println(s"$this decls: $decls") + + val ctors = decls.collect { case meth: MethodSymbol if meth.isConstructor => meth } + if (ctors.size != 1) { + reflectError(s"found multiple constructors ($ctors). " + + "Either remove all but the default constructor, or define a custom cloneType method.") + } + + val accessors = decls.collect { case meth: MethodSymbol if meth.isParamAccessor => meth } + val ctorParamss = ctors.head.paramLists + val ctorParams = ctorParamss match { + case Nil => List() + case ctorParams :: Nil => ctorParams + case _ => reflectError(s"internal error, unexpected ctorParamss = $ctorParamss") + List() // make the type system happy + } + + // Check that all ctor params are immutable and accessible. Immutability is required to avoid + // potential subtle bugs (like values changing after cloning). + // This also generates better error messages (all missing elements shown at once) instead of + // failing at the use site one at a time. + val ctorParamsName = ctorParams.map(_.name.toString) + val accessorsName = accessors.filter(_.isStable).map(_.name.toString) + val paramsDiff = ctorParamsName.toSet -- accessorsName.toSet + if (!paramsDiff.isEmpty) { + reflectError(s"constructor has parameters ($paramsDiff) that are not both immutable and accessible." + + " Either make all parameters immutable and accessible (vals) so cloneType can be inferred, or define a custom cloneType method.") } + + // Get all the argument values + val accessorsMap = accessors.map(accessor => accessor.name.toString -> accessor).toMap + val instanceReflect = mirror.reflect(this) + val ctorParamsVals = ctorParamsName.map { + paramName => instanceReflect.reflectMethod(accessorsMap(paramName)).apply() + } + + // Invoke ctor + val classReflect = mirror.reflectClass(mirror.classSymbol(this.getClass)) + classReflect.reflectConstructor(ctors.head).apply(ctorParamsVals:_*).asInstanceOf[this.type] + + + +// val constructor = this.getClass.getConstructors.head +// val args = Seq.fill(constructor.getParameterTypes.size)(null) +// constructor.newInstance(args:_*).asInstanceOf[this.type] + +// // If the user did not provide a cloneType method, try invoking one of +// // the following constructors, not all of which necessarily exist: +// // - A zero-parameter constructor +// // - A one-paramater constructor, with null as the argument +// // - A one-parameter constructor for a nested Bundle, with the enclosing +// // parent Module as the argument +// +// +// // val classMirror = runtimeMirror.reflectClass(classSymbol) +//// val constructors = classSymbol.typeSignature.members.filter(_.isConstructor).toList +//// println(constructors) +//// +// val constructor = this.getClass.getConstructors.head +// println(constructor) +// println(constructor.getParameterTypes) +// +// try { +// val args = Seq.fill(constructor.getParameterTypes.size)(null) +// constructor.newInstance(args:_*).asInstanceOf[this.type] +// } catch { +// case e: java.lang.reflect.InvocationTargetException if e.getCause.isInstanceOf[java.lang.NullPointerException] => +// try { +// constructor.newInstance(_parent.get).asInstanceOf[this.type] +// } catch { +// case _: java.lang.reflect.InvocationTargetException | _: java.lang.IllegalArgumentException => +// Builder.exception(s"Parameterized Bundle ${this.getClass} needs cloneType method. You are probably using " + +// "an anonymous Bundle object that captures external state and hence is un-cloneTypeable") +// this +// } +// case _: java.lang.reflect.InvocationTargetException | _: java.lang.IllegalArgumentException => +// Builder.exception(s"Parameterized Bundle ${this.getClass} needs cloneType method") +// this +// } } /** Default "pretty-print" implementation diff --git a/chiselFrontend/src/main/scala/chisel3/core/Binding.scala b/chiselFrontend/src/main/scala/chisel3/core/Binding.scala index 3fdc383c..a6cc8bac 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Binding.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Binding.scala @@ -23,7 +23,7 @@ object Binding { object requireIsHardware { def apply(node: Data, msg: String = "") = { node._parent match { // Compatibility layer hack - case Some(x: BaseModule) => x._autoWrapPorts + case Some(x: BaseModule) => x._compatAutoWrapPorts case _ => } if (!node.hasBinding) { diff --git a/chiselFrontend/src/main/scala/chisel3/core/BlackBox.scala b/chiselFrontend/src/main/scala/chisel3/core/BlackBox.scala index aa0f8064..3c99886e 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/BlackBox.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/BlackBox.scala @@ -126,12 +126,12 @@ abstract class ExtModule(val params: Map[String, Param] = Map.empty[String, Para */ abstract class BlackBox(val params: Map[String, Param] = Map.empty[String, Param])(implicit compileOptions: CompileOptions) extends BaseBlackBox { def io: Record - + // Allow access to bindings from the compatibility package - protected def _ioPortBound() = portsContains(io) - + protected def _compatIoPortBound() = portsContains(io) + private[core] override def generateComponent(): Component = { - _autoWrapPorts() // pre-IO(...) compatibility hack + _compatAutoWrapPorts() // pre-IO(...) compatibility hack // Restrict IO to just io, clock, and reset require(io != null, "BlackBox must have io") diff --git a/chiselFrontend/src/main/scala/chisel3/core/Module.scala b/chiselFrontend/src/main/scala/chisel3/core/Module.scala index fa9ab082..0c69010b 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Module.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Module.scala @@ -191,7 +191,7 @@ abstract class BaseModule extends HasId { * * TODO: remove this, perhaps by removing Bindings checks in compatibility mode. */ - def _autoWrapPorts() {} + def _compatAutoWrapPorts() {} // // BaseModule User API functions @@ -200,29 +200,10 @@ abstract class BaseModule extends HasId { Builder.annotations += annotation } - /** - * This must wrap the datatype used to set the io field of any Module. - * i.e. All concrete modules must have defined io in this form: - * [lazy] val io[: io type] = IO(...[: io type]) - * - * Items in [] are optional. - * - * The granted iodef WILL NOT be cloned (to allow for more seamless use of - * anonymous Bundles in the IO) and thus CANNOT have been bound to any logic. - * This will error if any node is bound (e.g. due to logic in a Bundle - * constructor, which is considered improper). - * - * Also registers a Data as a port, also performing bindings. Cannot be called once ports are - * requested (so that all calls to ports will return the same information). - * Internal API. - * - * TODO(twigg): Specifically walk the Data definition to call out which nodes - * are problematic. - */ - protected def IO[T<:Data](iodef: T): iodef.type = { - require(!_closed, "Can't add more ports after module close") - requireIsChiselType(iodef, "io type") - + /** Chisel2 code didn't require the IO(...) wrapper and would assign a Chisel type directly to + * io, then do operations on it. This binds a Chisel type in-place (mutably) as an IO. + */ + protected def _bindIoInPlace(iodef: Data) { // Compatibility code: Chisel2 did not require explicit direction on nodes // (unspecified treated as output, and flip on nothing was input). // This sets assigns the explicit directions required by newer semantics on @@ -248,11 +229,38 @@ abstract class BaseModule extends HasId { } } assignCompatDir(iodef, false) - - // Bind each element of the iodef to being a Port + iodef.bind(PortBinding(this)) _ports += iodef - iodef + } + + /** + * This must wrap the datatype used to set the io field of any Module. + * i.e. All concrete modules must have defined io in this form: + * [lazy] val io[: io type] = IO(...[: io type]) + * + * Items in [] are optional. + * + * The granted iodef WILL NOT be cloned (to allow for more seamless use of + * anonymous Bundles in the IO) and thus CANNOT have been bound to any logic. + * This will error if any node is bound (e.g. due to logic in a Bundle + * constructor, which is considered improper). + * + * Also registers a Data as a port, also performing bindings. Cannot be called once ports are + * requested (so that all calls to ports will return the same information). + * Internal API. + * + * TODO(twigg): Specifically walk the Data definition to call out which nodes + * are problematic. + */ + protected def IO[T<:Data](iodef: T): iodef.type = { + require(!_closed, "Can't add more ports after module close") + requireIsChiselType(iodef, "io type") + + // Clone the IO so we preserve immutability of data types + val iodefClone = iodef.cloneTypeFull + _bindIoInPlace(iodefClone) + iodefClone.asInstanceOf[iodef.type] } // diff --git a/chiselFrontend/src/main/scala/chisel3/core/UserModule.scala b/chiselFrontend/src/main/scala/chisel3/core/UserModule.scala index 9c923037..ac87eda5 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/UserModule.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/UserModule.scala @@ -151,8 +151,8 @@ abstract class LegacyModule(implicit moduleCompileOptions: CompileOptions) def io: Record // Allow access to bindings from the compatibility package - protected def _ioPortBound() = portsContains(io) - + protected def _compatIoPortBound() = portsContains(io) + protected override def nameIds(rootClass: Class[_]): HashMap[HasId, String] = { val names = super.nameIds(rootClass) @@ -165,7 +165,7 @@ abstract class LegacyModule(implicit moduleCompileOptions: CompileOptions) } private[core] override def generateComponent(): Component = { - _autoWrapPorts() // pre-IO(...) compatibility hack + _compatAutoWrapPorts() // pre-IO(...) compatibility hack // Restrict IO to just io, clock, and reset require(io != null, "Module must have io") diff --git a/src/main/scala/chisel3/compatibility.scala b/src/main/scala/chisel3/compatibility.scala index f299a287..3c12b483 100644 --- a/src/main/scala/chisel3/compatibility.scala +++ b/src/main/scala/chisel3/compatibility.scala @@ -250,9 +250,9 @@ package object Chisel { // scalastyle:ignore package.object.name abstract class BlackBox(params: Map[String, Param] = Map.empty[String, Param]) extends chisel3.core.BlackBox(params) { // This class auto-wraps the BlackBox with IO(...), allowing legacy code (where IO(...) wasn't // required) to build. - override def _autoWrapPorts(): Unit = { // scalastyle:ignore method.name - if (!_ioPortBound()) { - IO(io) + override def _compatAutoWrapPorts(): Unit = { // scalastyle:ignore method.name + if (!_compatIoPortBound()) { + _bindIoInPlace(io) } } } @@ -278,9 +278,9 @@ package object Chisel { // scalastyle:ignore package.object.name def this(_clock: Clock, _reset: Bool)(implicit moduleCompileOptions: CompileOptions) = this(Option(_clock), Option(_reset))(moduleCompileOptions) - override def _autoWrapPorts(): Unit = { // scalastyle:ignore method.name - if (!_ioPortBound() && io != null) { - IO(io) + override def _compatAutoWrapPorts(): Unit = { // scalastyle:ignore method.name + if (!_compatIoPortBound() && io != null) { + _bindIoInPlace(io) } } } diff --git a/src/main/scala/chisel3/testers/BasicTester.scala b/src/main/scala/chisel3/testers/BasicTester.scala index 6d1a4913..4cd03863 100644 --- a/src/main/scala/chisel3/testers/BasicTester.scala +++ b/src/main/scala/chisel3/testers/BasicTester.scala @@ -11,9 +11,14 @@ import internal.firrtl._ import internal.sourceinfo.SourceInfo //import chisel3.core.ExplicitCompileOptions.NotStrict -class BasicTester extends Module() { +class TesterIO extends Bundle { // The testbench has no IOs, rather it should communicate using printf, assert, and stop. - val io = IO(new Bundle()) + // This is here (instead of just `new Bundle()`, since that has an implicit compileOptions + // constructor argument which is misapplied by clonetype +} + +class BasicTester extends Module() { + val io = IO(new TesterIO) def popCount(n: Long): Int = n.toBinaryString.count(_=='1') diff --git a/src/test/scala/chiselTests/AutoClonetypeSpec.scala b/src/test/scala/chiselTests/AutoClonetypeSpec.scala new file mode 100644 index 00000000..93031c1c --- /dev/null +++ b/src/test/scala/chiselTests/AutoClonetypeSpec.scala @@ -0,0 +1,34 @@ +// See LICENSE for license details. + +package chiselTests + +import chisel3._ + +import chisel3.testers.BasicTester + +class BundleWithIntArg(val i: Int) extends Bundle { + val out = Output(UInt(i.W)) +} + +class ModuleWithInner extends Module { + class InnerBundle(val i: Int) extends Bundle { + val out = Output(UInt(i.W)) + } + + val io = IO(new InnerBundle(14)) + io.out := 1.U +} + + +class AutoClonetypeSpec extends ChiselFlatSpec { + "Bundles with Scala args" should "not need clonetype" in { + elaborate { new Module { + val io = IO(new BundleWithIntArg(8)) + io.out := 1.U + } } + } + + "Inner bundles with Scala args" should "not need clonetype" in { + elaborate { new ModuleWithInner } + } +} -- cgit v1.2.3 From 11c1112661e04094bccfd805e737e0318eb91ebc Mon Sep 17 00:00:00 2001 From: Albert Magyar Date: Wed, 22 Nov 2017 17:51:51 -0800 Subject: Add auto clone implementation for inner Bundles (#722) --- .../src/main/scala/chisel3/core/Aggregate.scala | 25 ++++++++ .../src/main/scala/chisel3/core/Data.scala | 7 +++ .../scala/chiselTests/AutoNestedCloneSpec.scala | 72 ++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 src/test/scala/chiselTests/AutoNestedCloneSpec.scala diff --git a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala index d4d67018..da14cefd 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala @@ -5,6 +5,7 @@ package chisel3.core import scala.collection.immutable.ListMap import scala.collection.mutable.{ArrayBuffer, HashSet, LinkedHashMap} import scala.language.experimental.macros +import scala.util.Try import chisel3.internal._ import chisel3.internal.Builder.pushCommand @@ -574,6 +575,30 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { Builder.exception(s"Unable to automatically infer cloneType on $this: $desc") } + // Check if the bundle is an instance of an inner class by examining + // whether it has one one-argument constructor taking a type matching the enclosing class + if (this.getClass.getConstructors.size == 1) { + val aggClass = this.getClass + val constr = aggClass.getConstructors.head + val argTypes = constr.getParameterTypes + val outerClass = aggClass.getEnclosingClass + if (argTypes.size == 1 && outerClass != null) { + // attempt to clone using "$outer" + var clone: Option[this.type] = + Try[this.type](constr.newInstance(aggClass.getDeclaredField("$outer").get(this)).asInstanceOf[this.type]).toOption + if (clone.isEmpty) { + // fall back to outerModule field + clone = Try[this.type](constr.newInstance(outerModule.get).asInstanceOf[this.type]).toOption + } + clone.foreach(_.outerModule = this.outerModule) + if (clone.isDefined) { + return clone.get + } else { + reflectError("non-trivial inner Bundle class") + } + } + } + // Try Scala reflection import scala.reflect.runtime.universe._ diff --git a/chiselFrontend/src/main/scala/chisel3/core/Data.scala b/chiselFrontend/src/main/scala/chisel3/core/Data.scala index 19adf01b..d84a86e9 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Data.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Data.scala @@ -208,6 +208,13 @@ abstract class Data extends HasId { } } + // If this Data is an instance of an inner class, record enclosing class + // This is only used for cloneType! + private[core] var outerModule: Option[BaseModule] = + (Option(this.getClass.getEnclosingClass) zip Builder.currentModule) + .find({ case (c, m) => c.isAssignableFrom(m.getClass) }) + .map({ case (_, m) => m }) + // User-specified direction, local at this node only. // Note that the actual direction of this node can differ from child and parent specifiedDirection. private var _specifiedDirection: SpecifiedDirection = SpecifiedDirection.Unspecified diff --git a/src/test/scala/chiselTests/AutoNestedCloneSpec.scala b/src/test/scala/chiselTests/AutoNestedCloneSpec.scala new file mode 100644 index 00000000..178cb142 --- /dev/null +++ b/src/test/scala/chiselTests/AutoNestedCloneSpec.scala @@ -0,0 +1,72 @@ +// See LICENSE for license details. + +package chiselTests +import Chisel.ChiselException +import org.scalatest._ +import chisel3._ + +class AutoNestedCloneSpec extends ChiselFlatSpec with Matchers { + behavior of "autoCloneType of inner Bundle in Chisel3" + + it should "clone a doubly-nested inner bundle successfully" in { + elaborate { + class Outer(val w: Int) extends Module { + class Middle(val w: Int) { + class InnerIOType extends Bundle { + val in = Input(UInt(w.W)) + } + def getIO = new InnerIOType + } + val io = IO((new Middle(w)).getIO) + } + new Outer(2) + } + } + + it should "clone an anonymous inner bundle successfully" in { + elaborate { + class TestTop(val w: Int) extends Module { + val io = IO(new Bundle{ val a = UInt(w.W) }) + } + new TestTop(2) + } + } + + it should "pick the correct $outer instance for an anonymous inner bundle" in { + elaborate { + class Inner(val w: Int) extends Module { + val io = IO(new Bundle{ + val in = Input(UInt(w.W)) + val out = Output(UInt(w.W)) + }) + } + class Outer(val w: Int) extends Module { + val io = IO(new Bundle{ + val in = Input(UInt(w.W)) + val out = Output(UInt(w.W)) + }) + val i = Module(new Inner(w)) + val iw = Wire(chiselTypeOf(i.io)) + iw <> io + i.io <> iw + } + new Outer(2) + } + } + + behavior of "anonymous doubly-nested inner bundle fails with clear error" + ( the[ChiselException] thrownBy { + elaborate { + class Outer(val w: Int) extends Module { + class Middle(val w: Int) { + def getIO = new Bundle { + val in = Input(UInt(w.W)) + } + } + val io = IO((new Middle(w)).getIO) + } + new Outer(2) + } + }).getMessage should include("non-trivial inner Bundle class") + +} -- cgit v1.2.3 From 48e30fab101c5552c73fc5a76cad3ccc6b38946f Mon Sep 17 00:00:00 2001 From: ducky64 Date: Wed, 22 Nov 2017 22:26:09 -0800 Subject: Support for inner classes, implicit parameter lists, supertypess --- .../src/main/scala/chisel3/core/Aggregate.scala | 224 ++++++++++++--------- .../src/main/scala/chisel3/core/Binding.scala | 2 +- .../src/main/scala/chisel3/core/BlackBox.scala | 8 +- .../src/main/scala/chisel3/core/Module.scala | 62 +++--- .../src/main/scala/chisel3/core/UserModule.scala | 6 +- src/main/scala/chisel3/compatibility.scala | 12 +- src/main/scala/chisel3/testers/BasicTester.scala | 9 +- .../scala/chiselTests/AnalogIntegrationSpec.scala | 2 +- src/test/scala/chiselTests/AutoClonetypeSpec.scala | 93 ++++++++- .../scala/chiselTests/AutoNestedCloneSpec.scala | 15 +- .../CompatibilityInteroperabilitySpec.scala | 4 + .../MissingCloneBindingExceptionSpec.scala | 4 +- .../scala/chiselTests/NamingAnnotationTest.scala | 2 +- src/test/scala/chiselTests/OptionBundle.scala | 4 +- src/test/scala/chiselTests/VectorPacketIO.scala | 2 +- .../scala/examples/VendingMachineGenerator.scala | 2 +- 16 files changed, 281 insertions(+), 170 deletions(-) diff --git a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala index da14cefd..ed3e1fee 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala @@ -5,7 +5,6 @@ package chisel3.core import scala.collection.immutable.ListMap import scala.collection.mutable.{ArrayBuffer, HashSet, LinkedHashMap} import scala.language.experimental.macros -import scala.util.Try import chisel3.internal._ import chisel3.internal.Builder.pushCommand @@ -558,131 +557,168 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { } override def cloneType : this.type = { - // Automatic Bundle cloning is inferred as follows: - // - User-overloaded cloneType is called instead of this (if supplied, so this method is never called) - // - Attempt to automatically fill in constructor parameters by using Scala reflection: - // - If multiple constructors, error out - // - If the constructor argument list can be matched to param accessors, call the constructor with - // the result of those accessors - // - Otherwise, error out - // - If Scala reflection crashes, fall back to Java reflection and inspect the constructor argument list: - // - If no parameters, invoke constructor directly. That was easy! - // - If the first parameter's type is equal to this Module, assume the Bundle is an inner class - // and invoke it with the current containing Module - // - Error out to the user - - def reflectError(desc: String) { - Builder.exception(s"Unable to automatically infer cloneType on $this: $desc") + // This attempts to infer constructor and arguments to clone this Bundle subtype without + // requiring the user explicitly overriding cloneType. + + def reflectError(desc: String): Nothing = { + Builder.exception(s"Unable to automatically infer cloneType on $this: $desc").asInstanceOf[Nothing] } - // Check if the bundle is an instance of an inner class by examining - // whether it has one one-argument constructor taking a type matching the enclosing class - if (this.getClass.getConstructors.size == 1) { - val aggClass = this.getClass - val constr = aggClass.getConstructors.head - val argTypes = constr.getParameterTypes - val outerClass = aggClass.getEnclosingClass - if (argTypes.size == 1 && outerClass != null) { - // attempt to clone using "$outer" - var clone: Option[this.type] = - Try[this.type](constr.newInstance(aggClass.getDeclaredField("$outer").get(this)).asInstanceOf[this.type]).toOption - if (clone.isEmpty) { - // fall back to outerModule field - clone = Try[this.type](constr.newInstance(outerModule.get).asInstanceOf[this.type]).toOption + import scala.reflect.runtime.universe._ + + // Check if this is an inner class, and if so, try to get the outer instance + val clazz = this.getClass + val outerClassInstance = Option(clazz.getEnclosingClass) match { + case Some(outerClass) => + val outerInstance = try { + clazz.getDeclaredField("$outer").get(this) // doesn't work in all cases, namely anonymous inner Bundles + } catch { + case _: NoSuchFieldException => this.outerModule match { + case Some(outerModule) => outerModule + case None => reflectError(s"Unable to determine instance of outer class $outerClass") + } } - clone.foreach(_.outerModule = this.outerModule) - if (clone.isDefined) { - return clone.get - } else { - reflectError("non-trivial inner Bundle class") + if (!outerClass.isAssignableFrom(outerInstance.getClass)) { + reflectError(s"Automatically determined outer class instance $outerInstance not assignable to outer class $outerClass") } - } + Some(outerClass, outerInstance) + case None => None } - // Try Scala reflection - import scala.reflect.runtime.universe._ + // If possible (constructor with no arguments), try Java reflection first + // This handles two cases that Scala reflection doesn't: + // 1. getting the ClassSymbol of a class with an anonymous outer class fails with a + // CyclicReference exception + // 2. invoking the constructor of an anonymous inner class seems broken (it expects the outer + // class as an argument, but fails because the number of arguments passed in is incorrect) + if (clazz.getConstructors.size == 1) { + var ctor = clazz.getConstructors.head + val argTypes = ctor.getParameterTypes.toList + val clone = (argTypes, outerClassInstance) match { + case (Nil, None) => // no arguments, no outer class, invoke constructor directly + Some(ctor.newInstance().asInstanceOf[this.type]) + case (argType :: Nil, Some((_, outerInstance))) => + if (argType isAssignableFrom outerInstance.getClass) { + Some(ctor.newInstance(outerInstance).asInstanceOf[this.type]) + } else { + None + } + case _ => None + } + clone match { + case Some(clone) => + clone.outerModule = this.outerModule + if (!clone.typeEquivalent(this)) { + reflectError(s"Automatically cloned $clone not type-equivalent to base $this." + + " Constructor argument values were not inferred, ensure constructor is deterministic.") + } + return clone.asInstanceOf[this.type] + case None => + } + } - val mirror = runtimeMirror(this.getClass.getClassLoader) - val decls = mirror.classSymbol(this.getClass).typeSignature.decls - println(s"$this decls: $decls") + // Get constructor parameters and accessible fields + val mirror = runtimeMirror(clazz.getClassLoader) + val classSymbol = try { + mirror.reflect(this).symbol + } catch { + case e: scala.reflect.internal.Symbols#CyclicReference => reflectError(s"Scala cannot reflect on $this, got exception $e." + + " This is known to occur with inner classes on anonymous outer classes." + + " In those cases, autoclonetype only works with no-argument constructors, or you can define a custom cloneType.") + } + val decls = classSymbol.typeSignature.decls val ctors = decls.collect { case meth: MethodSymbol if meth.isConstructor => meth } if (ctors.size != 1) { - reflectError(s"found multiple constructors ($ctors). " + - "Either remove all but the default constructor, or define a custom cloneType method.") + reflectError(s"found multiple constructors ($ctors)." + + " Either remove all but the default constructor, or define a custom cloneType method.") } - - val accessors = decls.collect { case meth: MethodSymbol if meth.isParamAccessor => meth } - val ctorParamss = ctors.head.paramLists + val ctor = ctors.head + val ctorParamss = ctor.paramLists val ctorParams = ctorParamss match { case Nil => List() case ctorParams :: Nil => ctorParams + case ctorParams :: ctorImplicits :: Nil => ctorParams ++ ctorImplicits case _ => reflectError(s"internal error, unexpected ctorParamss = $ctorParamss") - List() // make the type system happy } + val ctorParamsNames = ctorParams.map(_.name.toString) + + // Special case for anonymous inner classes: their constructor consists of just the outer class reference + // Scala reflection on anonymous inner class constructors seems broken + if (ctorParams.size == 1 && outerClassInstance.isDefined && + ctorParams.head.typeSignature == mirror.classSymbol(outerClassInstance.get._1).toType) { + // Fall back onto Java reflection + val ctors = clazz.getConstructors + require(ctors.size == 1) // should be consistent with Scala constructors + try { + val clone = ctors.head.newInstance(outerClassInstance.get._2).asInstanceOf[this.type] + clone.outerModule = this.outerModule + return clone + } catch { + case e @ (_: java.lang.reflect.InvocationTargetException | _: IllegalArgumentException) => + reflectError(s"Unexpected failure at constructor invocation, got $e.") + } + } + + // Get all the class symbols up to (but not including) Bundle and get all the accessors. + // (each ClassSymbol's decls only includes those declared in the class itself) + val bundleClassSymbol = mirror.classSymbol(classOf[Bundle]) + val superClassSymbols = classSymbol.baseClasses.takeWhile(_ != bundleClassSymbol) + val superClassDecls = superClassSymbols.map(_.typeSignature.decls).flatten + val accessors = superClassDecls.collect { case meth: MethodSymbol if meth.isParamAccessor => meth } + // Get constructor argument values // Check that all ctor params are immutable and accessible. Immutability is required to avoid // potential subtle bugs (like values changing after cloning). // This also generates better error messages (all missing elements shown at once) instead of // failing at the use site one at a time. - val ctorParamsName = ctorParams.map(_.name.toString) val accessorsName = accessors.filter(_.isStable).map(_.name.toString) - val paramsDiff = ctorParamsName.toSet -- accessorsName.toSet + val paramsDiff = ctorParamsNames.toSet -- accessorsName.toSet if (!paramsDiff.isEmpty) { - reflectError(s"constructor has parameters ($paramsDiff) that are not both immutable and accessible." + + reflectError(s"constructor has parameters $paramsDiff that are not both immutable and accessible." + " Either make all parameters immutable and accessible (vals) so cloneType can be inferred, or define a custom cloneType method.") } // Get all the argument values val accessorsMap = accessors.map(accessor => accessor.name.toString -> accessor).toMap val instanceReflect = mirror.reflect(this) - val ctorParamsVals = ctorParamsName.map { - paramName => instanceReflect.reflectMethod(accessorsMap(paramName)).apply() + val ctorParamsNameVals = ctorParamsNames.map { + paramName => paramName -> instanceReflect.reflectMethod(accessorsMap(paramName)).apply() + } + + // Oppurtunistic sanity check: ensure any arguments of type Data is not bound + // (which could lead to data conflicts, since it's likely the user didn't know to re-bind them). + // This is not guaranteed to catch all cases (for example, Data in Tuples or Iterables). + val boundDataParamNames = ctorParamsNameVals.map{ case (paramName, paramVal) => paramVal match { + case paramVal: Data => paramVal.hasBinding match { + case true => Some(paramName) + case false => None + } + case _ => None + } + }.filter(_.isDefined).map(_.get) + if (!boundDataParamNames.isEmpty) { + reflectError(s"constructor parameters ($boundDataParamNames) have values that are hardware types, which is likely to cause subtle errors." + + " Use chisel types instead: use the value before it is turned to a hardware type (with Wire(...), Reg(...), etc) or use chiselTypeOf(...) to extract the chisel type.") } + val ctorParamsVals = ctorParamsNameVals.map{ case (_, paramVal) => paramVal } + // Invoke ctor - val classReflect = mirror.reflectClass(mirror.classSymbol(this.getClass)) - classReflect.reflectConstructor(ctors.head).apply(ctorParamsVals:_*).asInstanceOf[this.type] - - - -// val constructor = this.getClass.getConstructors.head -// val args = Seq.fill(constructor.getParameterTypes.size)(null) -// constructor.newInstance(args:_*).asInstanceOf[this.type] - -// // If the user did not provide a cloneType method, try invoking one of -// // the following constructors, not all of which necessarily exist: -// // - A zero-parameter constructor -// // - A one-paramater constructor, with null as the argument -// // - A one-parameter constructor for a nested Bundle, with the enclosing -// // parent Module as the argument -// -// -// // val classMirror = runtimeMirror.reflectClass(classSymbol) -//// val constructors = classSymbol.typeSignature.members.filter(_.isConstructor).toList -//// println(constructors) -//// -// val constructor = this.getClass.getConstructors.head -// println(constructor) -// println(constructor.getParameterTypes) -// -// try { -// val args = Seq.fill(constructor.getParameterTypes.size)(null) -// constructor.newInstance(args:_*).asInstanceOf[this.type] -// } catch { -// case e: java.lang.reflect.InvocationTargetException if e.getCause.isInstanceOf[java.lang.NullPointerException] => -// try { -// constructor.newInstance(_parent.get).asInstanceOf[this.type] -// } catch { -// case _: java.lang.reflect.InvocationTargetException | _: java.lang.IllegalArgumentException => -// Builder.exception(s"Parameterized Bundle ${this.getClass} needs cloneType method. You are probably using " + -// "an anonymous Bundle object that captures external state and hence is un-cloneTypeable") -// this -// } -// case _: java.lang.reflect.InvocationTargetException | _: java.lang.IllegalArgumentException => -// Builder.exception(s"Parameterized Bundle ${this.getClass} needs cloneType method") -// this -// } + val classMirror = outerClassInstance match { + case Some((_, outerInstance)) => mirror.reflect(outerInstance).reflectClass(classSymbol) + case None => mirror.reflectClass(classSymbol) + } + val clone = classMirror.reflectConstructor(ctor).apply(ctorParamsVals:_*).asInstanceOf[this.type] + clone.outerModule = this.outerModule + + if (!clone.typeEquivalent(this)) { + reflectError(s"Automatically cloned $clone not type-equivalent to base $this." + + " Constructor argument values were inferred: ensure that variable names are consistent and have the same value throughout the constructor chain," + + " and that the constructor is deterministic.") + } + + clone } /** Default "pretty-print" implementation diff --git a/chiselFrontend/src/main/scala/chisel3/core/Binding.scala b/chiselFrontend/src/main/scala/chisel3/core/Binding.scala index a6cc8bac..3fdc383c 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Binding.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Binding.scala @@ -23,7 +23,7 @@ object Binding { object requireIsHardware { def apply(node: Data, msg: String = "") = { node._parent match { // Compatibility layer hack - case Some(x: BaseModule) => x._compatAutoWrapPorts + case Some(x: BaseModule) => x._autoWrapPorts case _ => } if (!node.hasBinding) { diff --git a/chiselFrontend/src/main/scala/chisel3/core/BlackBox.scala b/chiselFrontend/src/main/scala/chisel3/core/BlackBox.scala index 3c99886e..aa0f8064 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/BlackBox.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/BlackBox.scala @@ -126,12 +126,12 @@ abstract class ExtModule(val params: Map[String, Param] = Map.empty[String, Para */ abstract class BlackBox(val params: Map[String, Param] = Map.empty[String, Param])(implicit compileOptions: CompileOptions) extends BaseBlackBox { def io: Record - + // Allow access to bindings from the compatibility package - protected def _compatIoPortBound() = portsContains(io) - + protected def _ioPortBound() = portsContains(io) + private[core] override def generateComponent(): Component = { - _compatAutoWrapPorts() // pre-IO(...) compatibility hack + _autoWrapPorts() // pre-IO(...) compatibility hack // Restrict IO to just io, clock, and reset require(io != null, "BlackBox must have io") diff --git a/chiselFrontend/src/main/scala/chisel3/core/Module.scala b/chiselFrontend/src/main/scala/chisel3/core/Module.scala index 0c69010b..fa9ab082 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Module.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Module.scala @@ -191,7 +191,7 @@ abstract class BaseModule extends HasId { * * TODO: remove this, perhaps by removing Bindings checks in compatibility mode. */ - def _compatAutoWrapPorts() {} + def _autoWrapPorts() {} // // BaseModule User API functions @@ -200,10 +200,29 @@ abstract class BaseModule extends HasId { Builder.annotations += annotation } - /** Chisel2 code didn't require the IO(...) wrapper and would assign a Chisel type directly to - * io, then do operations on it. This binds a Chisel type in-place (mutably) as an IO. - */ - protected def _bindIoInPlace(iodef: Data) { + /** + * This must wrap the datatype used to set the io field of any Module. + * i.e. All concrete modules must have defined io in this form: + * [lazy] val io[: io type] = IO(...[: io type]) + * + * Items in [] are optional. + * + * The granted iodef WILL NOT be cloned (to allow for more seamless use of + * anonymous Bundles in the IO) and thus CANNOT have been bound to any logic. + * This will error if any node is bound (e.g. due to logic in a Bundle + * constructor, which is considered improper). + * + * Also registers a Data as a port, also performing bindings. Cannot be called once ports are + * requested (so that all calls to ports will return the same information). + * Internal API. + * + * TODO(twigg): Specifically walk the Data definition to call out which nodes + * are problematic. + */ + protected def IO[T<:Data](iodef: T): iodef.type = { + require(!_closed, "Can't add more ports after module close") + requireIsChiselType(iodef, "io type") + // Compatibility code: Chisel2 did not require explicit direction on nodes // (unspecified treated as output, and flip on nothing was input). // This sets assigns the explicit directions required by newer semantics on @@ -229,38 +248,11 @@ abstract class BaseModule extends HasId { } } assignCompatDir(iodef, false) - + + // Bind each element of the iodef to being a Port iodef.bind(PortBinding(this)) _ports += iodef - } - - /** - * This must wrap the datatype used to set the io field of any Module. - * i.e. All concrete modules must have defined io in this form: - * [lazy] val io[: io type] = IO(...[: io type]) - * - * Items in [] are optional. - * - * The granted iodef WILL NOT be cloned (to allow for more seamless use of - * anonymous Bundles in the IO) and thus CANNOT have been bound to any logic. - * This will error if any node is bound (e.g. due to logic in a Bundle - * constructor, which is considered improper). - * - * Also registers a Data as a port, also performing bindings. Cannot be called once ports are - * requested (so that all calls to ports will return the same information). - * Internal API. - * - * TODO(twigg): Specifically walk the Data definition to call out which nodes - * are problematic. - */ - protected def IO[T<:Data](iodef: T): iodef.type = { - require(!_closed, "Can't add more ports after module close") - requireIsChiselType(iodef, "io type") - - // Clone the IO so we preserve immutability of data types - val iodefClone = iodef.cloneTypeFull - _bindIoInPlace(iodefClone) - iodefClone.asInstanceOf[iodef.type] + iodef } // diff --git a/chiselFrontend/src/main/scala/chisel3/core/UserModule.scala b/chiselFrontend/src/main/scala/chisel3/core/UserModule.scala index ac87eda5..9c923037 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/UserModule.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/UserModule.scala @@ -151,8 +151,8 @@ abstract class LegacyModule(implicit moduleCompileOptions: CompileOptions) def io: Record // Allow access to bindings from the compatibility package - protected def _compatIoPortBound() = portsContains(io) - + protected def _ioPortBound() = portsContains(io) + protected override def nameIds(rootClass: Class[_]): HashMap[HasId, String] = { val names = super.nameIds(rootClass) @@ -165,7 +165,7 @@ abstract class LegacyModule(implicit moduleCompileOptions: CompileOptions) } private[core] override def generateComponent(): Component = { - _compatAutoWrapPorts() // pre-IO(...) compatibility hack + _autoWrapPorts() // pre-IO(...) compatibility hack // Restrict IO to just io, clock, and reset require(io != null, "Module must have io") diff --git a/src/main/scala/chisel3/compatibility.scala b/src/main/scala/chisel3/compatibility.scala index 3c12b483..f299a287 100644 --- a/src/main/scala/chisel3/compatibility.scala +++ b/src/main/scala/chisel3/compatibility.scala @@ -250,9 +250,9 @@ package object Chisel { // scalastyle:ignore package.object.name abstract class BlackBox(params: Map[String, Param] = Map.empty[String, Param]) extends chisel3.core.BlackBox(params) { // This class auto-wraps the BlackBox with IO(...), allowing legacy code (where IO(...) wasn't // required) to build. - override def _compatAutoWrapPorts(): Unit = { // scalastyle:ignore method.name - if (!_compatIoPortBound()) { - _bindIoInPlace(io) + override def _autoWrapPorts(): Unit = { // scalastyle:ignore method.name + if (!_ioPortBound()) { + IO(io) } } } @@ -278,9 +278,9 @@ package object Chisel { // scalastyle:ignore package.object.name def this(_clock: Clock, _reset: Bool)(implicit moduleCompileOptions: CompileOptions) = this(Option(_clock), Option(_reset))(moduleCompileOptions) - override def _compatAutoWrapPorts(): Unit = { // scalastyle:ignore method.name - if (!_compatIoPortBound() && io != null) { - _bindIoInPlace(io) + override def _autoWrapPorts(): Unit = { // scalastyle:ignore method.name + if (!_ioPortBound() && io != null) { + IO(io) } } } diff --git a/src/main/scala/chisel3/testers/BasicTester.scala b/src/main/scala/chisel3/testers/BasicTester.scala index 4cd03863..7bb441ba 100644 --- a/src/main/scala/chisel3/testers/BasicTester.scala +++ b/src/main/scala/chisel3/testers/BasicTester.scala @@ -11,14 +11,9 @@ import internal.firrtl._ import internal.sourceinfo.SourceInfo //import chisel3.core.ExplicitCompileOptions.NotStrict -class TesterIO extends Bundle { - // The testbench has no IOs, rather it should communicate using printf, assert, and stop. - // This is here (instead of just `new Bundle()`, since that has an implicit compileOptions - // constructor argument which is misapplied by clonetype -} - class BasicTester extends Module() { - val io = IO(new TesterIO) + // The testbench has no IOs, rather it should communicate using printf, assert, and stop. + val io = IO(new Bundle() {}) def popCount(n: Long): Int = n.toBinaryString.count(_=='1') diff --git a/src/test/scala/chiselTests/AnalogIntegrationSpec.scala b/src/test/scala/chiselTests/AnalogIntegrationSpec.scala index 952d3872..a3e6e643 100644 --- a/src/test/scala/chiselTests/AnalogIntegrationSpec.scala +++ b/src/test/scala/chiselTests/AnalogIntegrationSpec.scala @@ -19,7 +19,7 @@ class AnalogBlackBoxPort extends Bundle { // This IO can be used for a single BlackBox or to group multiple // Has multiple ports for driving and checking but only one shared bus -class AnalogBlackBoxIO(n: Int) extends Bundle { +class AnalogBlackBoxIO(val n: Int) extends Bundle { require(n > 0) val bus = Analog(32.W) val port = Vec(n, new AnalogBlackBoxPort) diff --git a/src/test/scala/chiselTests/AutoClonetypeSpec.scala b/src/test/scala/chiselTests/AutoClonetypeSpec.scala index 93031c1c..1170ed12 100644 --- a/src/test/scala/chiselTests/AutoClonetypeSpec.scala +++ b/src/test/scala/chiselTests/AutoClonetypeSpec.scala @@ -7,27 +7,108 @@ import chisel3._ import chisel3.testers.BasicTester class BundleWithIntArg(val i: Int) extends Bundle { - val out = Output(UInt(i.W)) + val out = UInt(i.W) +} + +class BundleWithImplicit()(implicit val ii: Int) extends Bundle { + val out = UInt(ii.W) +} + +class BundleWithArgAndImplicit(val i: Int)(implicit val ii: Int) extends Bundle { + val out1 = UInt(i.W) + val out2 = UInt(ii.W) +} + +class BaseBundleVal(val i: Int) extends Bundle { + val inner = UInt(i.W) +} +class SubBundle(i: Int, val i2: Int) extends BaseBundleVal(i) { + val inner2 = UInt(i2.W) +} +class SubBundleInvalid(i: Int, val i2: Int) extends BaseBundleVal(i+1) { + val inner2 = UInt(i2.W) +} + +class BaseBundleNonVal(i: Int) extends Bundle { + val inner = UInt(i.W) +} +class SubBundleVal(val i: Int, val i2: Int) extends BaseBundleNonVal(i) { + val inner2 = UInt(i2.W) } class ModuleWithInner extends Module { class InnerBundle(val i: Int) extends Bundle { - val out = Output(UInt(i.W)) + val out = UInt(i.W) } - val io = IO(new InnerBundle(14)) - io.out := 1.U + val io = IO(new Bundle{}) + + val myWire = Wire(new InnerBundle(14)) + require(myWire.i == 14) } class AutoClonetypeSpec extends ChiselFlatSpec { "Bundles with Scala args" should "not need clonetype" in { elaborate { new Module { - val io = IO(new BundleWithIntArg(8)) - io.out := 1.U + val io = IO(new Bundle{}) + + val myWire = Wire(new BundleWithIntArg(8)) + assert(myWire.i == 8) + } } + } + + "Bundles with Scala implicit args" should "not need clonetype" in { + elaborate { new Module { + val io = IO(new Bundle{}) + + implicit val implicitInt: Int = 4 + val myWire = Wire(new BundleWithImplicit()) + + assert(myWire.ii == 4) } } } + "Bundles with Scala explicit and impicit args" should "not need clonetype" in { + elaborate { new Module { + val io = IO(new Bundle{}) + + implicit val implicitInt: Int = 4 + val myWire = Wire(new BundleWithArgAndImplicit(8)) + + assert(myWire.i == 8) + assert(myWire.ii == 4) + } } + } + + "Subtyped Bundles" should "not need clonetype" in { + elaborate { new Module { + val io = IO(new Bundle{}) + + val myWire = Wire(new SubBundle(8, 4)) + + assert(myWire.i == 8) + assert(myWire.i2 == 4) + } } + elaborate { new Module { + val io = IO(new Bundle{}) + + val myWire = Wire(new SubBundleVal(8, 4)) + + assert(myWire.i == 8) + assert(myWire.i2 == 4) + } } + } + + "Subtyped Bundles that don't clone well" should "be caught" in { + a [ChiselException] should be thrownBy { + elaborate { new Module { + val io = IO(new Bundle{}) + val myWire = Wire(new SubBundleInvalid(8, 4)) + } } + } + } + "Inner bundles with Scala args" should "not need clonetype" in { elaborate { new ModuleWithInner } } diff --git a/src/test/scala/chiselTests/AutoNestedCloneSpec.scala b/src/test/scala/chiselTests/AutoNestedCloneSpec.scala index 178cb142..d3977213 100644 --- a/src/test/scala/chiselTests/AutoNestedCloneSpec.scala +++ b/src/test/scala/chiselTests/AutoNestedCloneSpec.scala @@ -9,7 +9,7 @@ class AutoNestedCloneSpec extends ChiselFlatSpec with Matchers { behavior of "autoCloneType of inner Bundle in Chisel3" it should "clone a doubly-nested inner bundle successfully" in { - elaborate { + elaborate { class Outer(val w: Int) extends Module { class Middle(val w: Int) { class InnerIOType extends Bundle { @@ -17,16 +17,18 @@ class AutoNestedCloneSpec extends ChiselFlatSpec with Matchers { } def getIO = new InnerIOType } - val io = IO((new Middle(w)).getIO) + val io = IO(new Bundle {}) + val myWire = Wire((new Middle(w)).getIO) } new Outer(2) } } it should "clone an anonymous inner bundle successfully" in { - elaborate { + elaborate { class TestTop(val w: Int) extends Module { - val io = IO(new Bundle{ val a = UInt(w.W) }) + val io = IO(new Bundle {}) + val myWire = Wire(new Bundle{ val a = UInt(w.W) }) } new TestTop(2) } @@ -63,10 +65,11 @@ class AutoNestedCloneSpec extends ChiselFlatSpec with Matchers { val in = Input(UInt(w.W)) } } - val io = IO((new Middle(w)).getIO) + val io = IO(new Bundle {}) + val myWire = Wire((new Middle(w)).getIO) } new Outer(2) } - }).getMessage should include("non-trivial inner Bundle class") + }).getMessage should include("Unable to determine instance") } diff --git a/src/test/scala/chiselTests/CompatibilityInteroperabilitySpec.scala b/src/test/scala/chiselTests/CompatibilityInteroperabilitySpec.scala index 457f26de..95dc87c1 100644 --- a/src/test/scala/chiselTests/CompatibilityInteroperabilitySpec.scala +++ b/src/test/scala/chiselTests/CompatibilityInteroperabilitySpec.scala @@ -12,6 +12,8 @@ object CompatibilityComponents { class ChiselBundle extends Bundle { val a = UInt(width = 32) val b = UInt(width = 32).flip + + override def cloneType = (new ChiselBundle).asInstanceOf[this.type] } class ChiselRecord extends Record { val elements = ListMap("a" -> UInt(width = 32), "b" -> UInt(width = 32).flip) @@ -46,6 +48,8 @@ object Chisel3Components { class Chisel3Bundle extends Bundle { val a = Output(UInt(32.W)) val b = Input(UInt(32.W)) + + override def cloneType = (new Chisel3Bundle).asInstanceOf[this.type] } class Chisel3Record extends Record { diff --git a/src/test/scala/chiselTests/MissingCloneBindingExceptionSpec.scala b/src/test/scala/chiselTests/MissingCloneBindingExceptionSpec.scala index 43f2b0fd..fc9346a0 100644 --- a/src/test/scala/chiselTests/MissingCloneBindingExceptionSpec.scala +++ b/src/test/scala/chiselTests/MissingCloneBindingExceptionSpec.scala @@ -28,7 +28,7 @@ class MissingCloneBindingExceptionSpec extends ChiselFlatSpec with Matchers { } elaborate(new TestTop) - }).getMessage should include("needs cloneType method") + }).getMessage should include("make all parameters immutable") behavior of "missing cloneType in Chisel2" ( the[ChiselException] thrownBy { @@ -53,5 +53,5 @@ class MissingCloneBindingExceptionSpec extends ChiselFlatSpec with Matchers { } elaborate(new TestTop) - }).getMessage should include("needs cloneType method") + }).getMessage should include("make all parameters immutable") } diff --git a/src/test/scala/chiselTests/NamingAnnotationTest.scala b/src/test/scala/chiselTests/NamingAnnotationTest.scala index 699dc0fb..a7b9b75c 100644 --- a/src/test/scala/chiselTests/NamingAnnotationTest.scala +++ b/src/test/scala/chiselTests/NamingAnnotationTest.scala @@ -12,7 +12,7 @@ import chisel3.testers.BasicTester import scala.collection.mutable.ListBuffer trait NamedModuleTester extends Module { - val io = IO(new Bundle()) // Named module testers don't need IO + val io = IO(new Bundle() {}) // Named module testers don't need IO val expectedNameMap = ListBuffer[(InstanceId, String)]() val expectedModuleNameMap = ListBuffer[(Module, String)]() diff --git a/src/test/scala/chiselTests/OptionBundle.scala b/src/test/scala/chiselTests/OptionBundle.scala index 2ac661ea..03b08385 100644 --- a/src/test/scala/chiselTests/OptionBundle.scala +++ b/src/test/scala/chiselTests/OptionBundle.scala @@ -6,7 +6,7 @@ import org.scalatest._ import chisel3._ import chisel3.testers.BasicTester -class OptionBundle(hasIn: Boolean) extends Bundle { +class OptionBundle(val hasIn: Boolean) extends Bundle { val in = if (hasIn) { Some(Input(Bool())) } else { @@ -15,7 +15,7 @@ class OptionBundle(hasIn: Boolean) extends Bundle { val out = Output(Bool()) } -class OptionBundleModule(hasIn: Boolean) extends Module { +class OptionBundleModule(val hasIn: Boolean) extends Module { val io = IO(new OptionBundle(hasIn)) if (hasIn) { io.out := io.in.get diff --git a/src/test/scala/chiselTests/VectorPacketIO.scala b/src/test/scala/chiselTests/VectorPacketIO.scala index 7745db57..4a8c6f78 100644 --- a/src/test/scala/chiselTests/VectorPacketIO.scala +++ b/src/test/scala/chiselTests/VectorPacketIO.scala @@ -27,7 +27,7 @@ class Packet extends Bundle { * lines also. * The problem does not occur if the Vec is taken out */ -class VectorPacketIO(n: Int) extends Bundle { +class VectorPacketIO(val n: Int) extends Bundle { val ins = Vec(n, chisel3.util.DeqIO(new Packet())) val outs = Vec(n, chisel3.util.EnqIO(new Packet())) } diff --git a/src/test/scala/examples/VendingMachineGenerator.scala b/src/test/scala/examples/VendingMachineGenerator.scala index 55a17d9f..0a9dc3e6 100644 --- a/src/test/scala/examples/VendingMachineGenerator.scala +++ b/src/test/scala/examples/VendingMachineGenerator.scala @@ -9,7 +9,7 @@ import chisel3.util._ import VendingMachineUtils._ -class VendingMachineIO(legalCoins: Seq[Coin]) extends Bundle { +class VendingMachineIO(val legalCoins: Seq[Coin]) extends Bundle { require(legalCoins.size >= 1, "The vending machine must accept at least 1 coin!") // Order of coins by value val coins: Seq[Coin] = legalCoins sortBy (_.value) -- cgit v1.2.3 From 43e4d6dbd0f131c7484dc24cb09ab3a25614cb62 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Fri, 22 Dec 2017 01:21:41 -0800 Subject: address review style comments --- .../src/main/scala/chisel3/core/Aggregate.scala | 43 +++++++++------------- .../src/main/scala/chisel3/core/Data.scala | 8 ++-- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala index ed3e1fee..43e430cd 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala @@ -559,6 +559,7 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { override def cloneType : this.type = { // This attempts to infer constructor and arguments to clone this Bundle subtype without // requiring the user explicitly overriding cloneType. + import scala.language.existentials def reflectError(desc: String): Nothing = { Builder.exception(s"Unable to automatically infer cloneType on $this: $desc").asInstanceOf[Nothing] @@ -568,21 +569,18 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { // Check if this is an inner class, and if so, try to get the outer instance val clazz = this.getClass - val outerClassInstance = Option(clazz.getEnclosingClass) match { - case Some(outerClass) => - val outerInstance = try { - clazz.getDeclaredField("$outer").get(this) // doesn't work in all cases, namely anonymous inner Bundles - } catch { - case _: NoSuchFieldException => this.outerModule match { - case Some(outerModule) => outerModule - case None => reflectError(s"Unable to determine instance of outer class $outerClass") - } - } - if (!outerClass.isAssignableFrom(outerInstance.getClass)) { - reflectError(s"Automatically determined outer class instance $outerInstance not assignable to outer class $outerClass") - } - Some(outerClass, outerInstance) - case None => None + val outerClassInstance = Option(clazz.getEnclosingClass).map { outerClass => + val outerInstance = try { + clazz.getDeclaredField("$outer").get(this) // doesn't work in all cases, namely anonymous inner Bundles + } catch { + case _: NoSuchFieldException => + this.outerModule.getOrElse(reflectError(s"Unable to determine instance of outer class $outerClass")) + } + if (!outerClass.isAssignableFrom(outerInstance.getClass)) { + reflectError(s"Unable to determine instance of outer class $outerClass," + + s" guessed $outerInstance, but is not assignable to outer class $outerClass") + } + (outerClass, outerInstance) } // If possible (constructor with no arguments), try Java reflection first @@ -686,18 +684,13 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { paramName => paramName -> instanceReflect.reflectMethod(accessorsMap(paramName)).apply() } - // Oppurtunistic sanity check: ensure any arguments of type Data is not bound + // Opportunistic sanity check: ensure any arguments of type Data is not bound // (which could lead to data conflicts, since it's likely the user didn't know to re-bind them). // This is not guaranteed to catch all cases (for example, Data in Tuples or Iterables). - val boundDataParamNames = ctorParamsNameVals.map{ case (paramName, paramVal) => paramVal match { - case paramVal: Data => paramVal.hasBinding match { - case true => Some(paramName) - case false => None - } - case _ => None - } - }.filter(_.isDefined).map(_.get) - if (!boundDataParamNames.isEmpty) { + val boundDataParamNames = ctorParamsNameVals.collect { + case (paramName, paramVal: Data) if paramVal.hasBinding => paramName + } + if (boundDataParamNames.nonEmpty) { reflectError(s"constructor parameters ($boundDataParamNames) have values that are hardware types, which is likely to cause subtle errors." + " Use chisel types instead: use the value before it is turned to a hardware type (with Wire(...), Reg(...), etc) or use chiselTypeOf(...) to extract the chisel type.") } diff --git a/chiselFrontend/src/main/scala/chisel3/core/Data.scala b/chiselFrontend/src/main/scala/chisel3/core/Data.scala index d84a86e9..35401fa1 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Data.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Data.scala @@ -209,11 +209,9 @@ abstract class Data extends HasId { } // If this Data is an instance of an inner class, record enclosing class - // This is only used for cloneType! - private[core] var outerModule: Option[BaseModule] = - (Option(this.getClass.getEnclosingClass) zip Builder.currentModule) - .find({ case (c, m) => c.isAssignableFrom(m.getClass) }) - .map({ case (_, m) => m }) + // This is only used for cloneType, and is mutable to allow autoCloneType to preserve the + // original outerModule. + private[core] var outerModule: Option[BaseModule] = Builder.currentModule // User-specified direction, local at this node only. // Note that the actual direction of this node can differ from child and parent specifiedDirection. -- cgit v1.2.3 From ade792ee7c5bb718f738f5e4c3886b2e87c68756 Mon Sep 17 00:00:00 2001 From: Jack Koenig Date: Thu, 28 Dec 2017 16:56:05 -0800 Subject: Add support for autoclonetype of bound, anonymous inner Bundles Also change Data.outerModule to Bundle._outerInst since it is only used in autoclonetype. _outerInst is also Option[Object] instead of Option[BaseModule] because the outer object could also be a Bundle. --- .../src/main/scala/chisel3/core/Aggregate.scala | 21 +++++++++---- .../src/main/scala/chisel3/core/Data.scala | 6 +--- .../scala/chiselTests/AutoNestedCloneSpec.scala | 35 +++++++++++++++++++++- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala index 43e430cd..14011cd9 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala @@ -556,6 +556,11 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { case _ => None } + // If this Data is an instance of an inner class, record a *guess* at the outer object. + // This is only used for cloneType, and is mutable to allow autoCloneType to try guesses other + // than the module that was current when the Bundle was constructed. + private var _outerInst: Option[Object] = Builder.currentModule + override def cloneType : this.type = { // This attempts to infer constructor and arguments to clone this Bundle subtype without // requiring the user explicitly overriding cloneType. @@ -570,16 +575,22 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { // Check if this is an inner class, and if so, try to get the outer instance val clazz = this.getClass val outerClassInstance = Option(clazz.getEnclosingClass).map { outerClass => + def canAssignOuterClass(x: Object) = outerClass.isAssignableFrom(x.getClass) val outerInstance = try { clazz.getDeclaredField("$outer").get(this) // doesn't work in all cases, namely anonymous inner Bundles } catch { case _: NoSuchFieldException => - this.outerModule.getOrElse(reflectError(s"Unable to determine instance of outer class $outerClass")) + // First check if this Bundle is bound and an anonymous inner Bundle of another Bundle + this.bindingOpt.collect { case ChildBinding(p) if canAssignOuterClass(p) => p } + .orElse(this._outerInst) + .getOrElse(reflectError(s"Unable to determine instance of outer class $outerClass")) } - if (!outerClass.isAssignableFrom(outerInstance.getClass)) { + if (!canAssignOuterClass(outerInstance)) { reflectError(s"Unable to determine instance of outer class $outerClass," + s" guessed $outerInstance, but is not assignable to outer class $outerClass") } + // Record the outer instance for future cloning + this._outerInst = Some(outerInstance) (outerClass, outerInstance) } @@ -605,7 +616,7 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { } clone match { case Some(clone) => - clone.outerModule = this.outerModule + clone._outerInst = this._outerInst if (!clone.typeEquivalent(this)) { reflectError(s"Automatically cloned $clone not type-equivalent to base $this." + " Constructor argument values were not inferred, ensure constructor is deterministic.") @@ -650,7 +661,7 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { require(ctors.size == 1) // should be consistent with Scala constructors try { val clone = ctors.head.newInstance(outerClassInstance.get._2).asInstanceOf[this.type] - clone.outerModule = this.outerModule + clone._outerInst = this._outerInst return clone } catch { case e @ (_: java.lang.reflect.InvocationTargetException | _: IllegalArgumentException) => @@ -703,7 +714,7 @@ class Bundle(implicit compileOptions: CompileOptions) extends Record { case None => mirror.reflectClass(classSymbol) } val clone = classMirror.reflectConstructor(ctor).apply(ctorParamsVals:_*).asInstanceOf[this.type] - clone.outerModule = this.outerModule + clone._outerInst = this._outerInst if (!clone.typeEquivalent(this)) { reflectError(s"Automatically cloned $clone not type-equivalent to base $this." + diff --git a/chiselFrontend/src/main/scala/chisel3/core/Data.scala b/chiselFrontend/src/main/scala/chisel3/core/Data.scala index 35401fa1..1cf50e9f 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Data.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Data.scala @@ -208,11 +208,6 @@ abstract class Data extends HasId { } } - // If this Data is an instance of an inner class, record enclosing class - // This is only used for cloneType, and is mutable to allow autoCloneType to preserve the - // original outerModule. - private[core] var outerModule: Option[BaseModule] = Builder.currentModule - // User-specified direction, local at this node only. // Note that the actual direction of this node can differ from child and parent specifiedDirection. private var _specifiedDirection: SpecifiedDirection = SpecifiedDirection.Unspecified @@ -245,6 +240,7 @@ abstract class Data extends HasId { // This information is supplemental (more than is necessary to generate FIRRTL) and is used to // perform checks in Chisel, where more informative error messages are possible. private var _binding: Option[Binding] = None + private[core] def bindingOpt = _binding private[core] def hasBinding = _binding.isDefined // Only valid after node is bound (synthesizable), crashes otherwise private[core] def binding = _binding.get diff --git a/src/test/scala/chiselTests/AutoNestedCloneSpec.scala b/src/test/scala/chiselTests/AutoNestedCloneSpec.scala index d3977213..746780be 100644 --- a/src/test/scala/chiselTests/AutoNestedCloneSpec.scala +++ b/src/test/scala/chiselTests/AutoNestedCloneSpec.scala @@ -5,6 +5,12 @@ import Chisel.ChiselException import org.scalatest._ import chisel3._ +class BundleWithAnonymousInner(val w: Int) extends Bundle { + val inner = new Bundle { + val foo = Input(UInt(w.W)) + } +} + class AutoNestedCloneSpec extends ChiselFlatSpec with Matchers { behavior of "autoCloneType of inner Bundle in Chisel3" @@ -18,7 +24,7 @@ class AutoNestedCloneSpec extends ChiselFlatSpec with Matchers { def getIO = new InnerIOType } val io = IO(new Bundle {}) - val myWire = Wire((new Middle(w)).getIO) + val myWire = Wire((new Middle(w)).getIO) } new Outer(2) } @@ -56,6 +62,33 @@ class AutoNestedCloneSpec extends ChiselFlatSpec with Matchers { } } + it should "clone an anonymous, bound, inner bundle of another bundle successfully" in { + elaborate { + class TestModule(w: Int) extends Module { + val io = IO(new BundleWithAnonymousInner(w) ) + val w0 = WireInit(io) + val w1 = WireInit(io.inner) + } + new TestModule(8) + } + } + + it should "clone an anonymous, inner bundle of a Module, bound to another bundle successfully" in { + elaborate { + class TestModule(w: Int) extends Module { + val bun = new Bundle { + val foo = UInt(w.W) + } + val io = IO(new Bundle { + val inner = Input(bun) + }) + val w0 = WireInit(io) + val w1 = WireInit(io.inner) + } + new TestModule(8) + } + } + behavior of "anonymous doubly-nested inner bundle fails with clear error" ( the[ChiselException] thrownBy { elaborate { -- cgit v1.2.3