diff options
| -rw-r--r-- | build.sbt | 13 | ||||
| -rw-r--r-- | chiselFrontend/src/main/scala/chisel3/internal/Builder.scala | 10 | ||||
| -rw-r--r-- | chiselFrontend/src/main/scala/chisel3/internal/Namer.scala | 151 | ||||
| -rw-r--r-- | coreMacros/src/main/scala/chisel3/internal/sourceinfo/NamingAnnotations.scala | 180 | ||||
| -rw-r--r-- | src/main/scala/chisel3/compatibility.scala | 27 | ||||
| -rw-r--r-- | src/main/scala/chisel3/package.scala | 17 | ||||
| -rw-r--r-- | src/test/scala/chiselTests/NamingAnnotationTest.scala | 200 |
7 files changed, 589 insertions, 9 deletions
@@ -22,6 +22,8 @@ lazy val commonSettings = Seq ( autoAPIMappings := true, scalaVersion := "2.11.7", scalacOptions := Seq("-deprecation", "-feature"), + libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value, + addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full), // Since we want to examine the classpath to determine if a dependency on firrtl is required, // this has to be a Task setting. // Fortunately, allDependencies is a Task Setting, so we can modify that. @@ -78,7 +80,6 @@ lazy val chiselSettings = Seq ( libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "2.2.5" % "test", - "org.scala-lang" % "scala-reflect" % scalaVersion.value, "org.scalacheck" %% "scalacheck" % "1.12.4" % "test", "com.github.scopt" %% "scopt" % "3.4.0" ), @@ -96,17 +97,11 @@ lazy val chiselSettings = Seq ( lazy val coreMacros = (project in file("coreMacros")). settings(commonSettings: _*). - settings( - libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value, - publishArtifact := false - ) + settings(publishArtifact := false) lazy val chiselFrontend = (project in file("chiselFrontend")). settings(commonSettings: _*). - settings( - libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value, - publishArtifact := false - ). + settings(publishArtifact := false). dependsOn(coreMacros) diff --git a/chiselFrontend/src/main/scala/chisel3/internal/Builder.scala b/chiselFrontend/src/main/scala/chisel3/internal/Builder.scala index c93dbfc7..5d841941 100644 --- a/chiselFrontend/src/main/scala/chisel3/internal/Builder.scala +++ b/chiselFrontend/src/main/scala/chisel3/internal/Builder.scala @@ -151,6 +151,7 @@ private[chisel3] class DynamicContext() { // Used to distinguish between no Module() wrapping, multiple wrappings, and rewrapping var readyForModuleConstr: Boolean = false val errors = new ErrorLog + val namingStack = new internal.naming.NamingStack } private[chisel3] object Builder { @@ -163,6 +164,7 @@ private[chisel3] object Builder { def globalNamespace: Namespace = dynamicContext.globalNamespace def components: ArrayBuffer[Component] = dynamicContext.components def annotations: ArrayBuffer[ChiselAnnotation] = dynamicContext.annotations + def namingStack: internal.naming.NamingStack = dynamicContext.namingStack def currentModule: Option[Module] = dynamicContext.currentModule def currentModule_=(target: Option[Module]): Unit = { @@ -222,3 +224,11 @@ private[chisel3] object Builder { } } } + +/** Allows public access to the naming stack in Builder / DynamicContext. + * Necessary because naming macros expand in user code and don't have access into private[chisel3] + * objects. + */ +object DynamicNamingStack { + def apply() = Builder.namingStack +} diff --git a/chiselFrontend/src/main/scala/chisel3/internal/Namer.scala b/chiselFrontend/src/main/scala/chisel3/internal/Namer.scala new file mode 100644 index 00000000..a7196a22 --- /dev/null +++ b/chiselFrontend/src/main/scala/chisel3/internal/Namer.scala @@ -0,0 +1,151 @@ +// See LICENSE for license details. + +// This file contains part of the implementation of the naming static annotation system. + +package chisel3.internal.naming + +import scala.collection.mutable.Stack +import scala.collection.mutable.ListBuffer + +import scala.collection.JavaConversions._ + +import java.util.IdentityHashMap + +/** Recursive Function Namer overview + * + * In every function, creates a NamingContext object, which associates all vals with a string name + * suffix, for example: + * val myValName = SomeStatement() + * produces the entry in items: + * {ref of SomeStatement(), "myValName"} + * + * This is achieved with a macro transforming: + * val myValName = SomeStatement() + * statements into a naming call: + * val myValName = context.name(SomeStatement(), "myValName") + * + * The context is created from a global dynamic context stack at the beginning of each function. + * At the end of each function call, the completed context is added to its parent context and + * associated with the return value (whose name at an enclosing function call will form the prefix + * for all named objects). + * + * When the naming context prefix is given, it will name all of its items with the prefix and the + * associated suffix name. Then, it will check its descendants for sub-contexts with references + * matching the item reference, and if there is a match, it will (recursively) give the + * sub-context a prefix of its current prefix plus the item reference suffix. + * + * Note that for Modules, the macro will insert a naming context prefix call with an empty prefix, + * starting the recursive naming process. + */ + +/** Base class for naming contexts, providing the basic API consisting of naming calls and + * ability to take descendant naming contexts. + */ +class NamingContext { + val descendants = new IdentityHashMap[AnyRef, ListBuffer[NamingContext]]() + val anonymousDescendants = ListBuffer[NamingContext]() + val items = ListBuffer[(AnyRef, String)]() + var closed = false // a sanity check to ensure no more name() calls are done after name_prefix + + /** Adds a NamingContext object as a descendant - where its contained objects will have names + * prefixed with the name given to the reference object, if the reference object is named in the + * scope of this context. + */ + def add_descendant(ref: AnyRef, descendant: NamingContext) { + if (!descendants.containsKey(ref)) { + descendants.put(ref, ListBuffer[NamingContext]()) + } + descendants.get(ref) += descendant + } + + def add_anonymous_descendant(descendant: NamingContext) { + anonymousDescendants += descendant + } + + /** Suggest a name (that will be propagated to FIRRTL) for an object, then returns the object + * itself (so this can be inserted transparently anywhere). + * Is a no-op (so safe) when applied on objects that aren't named, including non-Chisel data + * types. + */ + def name[T](obj: T, name: String): T = { + assert(!closed, "Can't name elements after name_prefix called") + obj match { + case ref: AnyRef => items += ((ref, name)) + case _ => + } + obj + } + + /** Gives this context a naming prefix (which may be empty, "", for a top-level Module context) + * so that actual naming calls (HasId.suggestName) can happen. + * Recursively names descendants, for those whose return value have an associated name. + */ + def name_prefix(prefix: String) { + closed = true + for ((ref, suffix) <- items) { + // First name the top-level object + ref match { + case nameable: chisel3.internal.HasId => nameable.suggestName(prefix + suffix) + case _ => + } + + // Then recurse into descendant contexts + if (descendants.containsKey(ref)) { + for (descendant <- descendants.get(ref)) { + descendant.name_prefix(prefix + suffix + "_") + } + descendants.remove(ref) + } + } + + for (descendant <- descendants.values().flatten) { + // Where we have a broken naming link, just ignore the missing parts + descendant.name_prefix(prefix) + } + for (descendant <- anonymousDescendants) { + descendant.name_prefix(prefix) + } + } +} + +/** Class for the (global) naming stack object, which provides a way to push and pop naming + * contexts as functions are called / finished. + */ +class NamingStack { + val naming_stack = Stack[NamingContext]() + + /** Creates a new naming context, where all items in the context will have their names prefixed + * with some yet-to-be-determined prefix from object names in an enclosing scope. + */ + def push_context(): NamingContext = { + val context = new NamingContext + naming_stack.push(context) + context + } + + /** Called at the end of a function, popping the current naming context, adding it to the + * enclosing context's descendants, and passing through the prefix naming reference. + * Every instance of push_context() must have a matching pop_context(). + * + * Will assert out if the context being popped isn't the topmost on the stack. + */ + def pop_return_context[T <: Any](prefix_ref: T, until: NamingContext): T = { + assert(naming_stack.top == until) + naming_stack.pop() + if (!naming_stack.isEmpty) { + prefix_ref match { + case prefix_ref: AnyRef => naming_stack.top.add_descendant(prefix_ref, until) + case _ => naming_stack.top.add_anonymous_descendant(until) + } + + } + prefix_ref + } + + /** Same as pop_return_context, but for cases where there is no return value (like Module scope). + */ + def pop_context(until: NamingContext) { + assert(naming_stack.top == until) + naming_stack.pop() + } +} diff --git a/coreMacros/src/main/scala/chisel3/internal/sourceinfo/NamingAnnotations.scala b/coreMacros/src/main/scala/chisel3/internal/sourceinfo/NamingAnnotations.scala new file mode 100644 index 00000000..1f7c1cac --- /dev/null +++ b/coreMacros/src/main/scala/chisel3/internal/sourceinfo/NamingAnnotations.scala @@ -0,0 +1,180 @@ +// See LICENSE for license details. + +// Transform implementations for name-propagation related annotations. +// +// Helpful references: +// http://docs.scala-lang.org/overviews/quasiquotes/syntax-summary.html#definitions +// for quasiquote structures of various Scala structures +// http://jsuereth.com/scala/2009/02/05/leveraging-annotations-in-scala.html +// use of Transformer +// http://www.scala-lang.org/old/sites/default/files/sids/rytz/Wed,%202010-01-27,%2015:10/annots.pdf +// general annotations reference + +package chisel3.internal.naming + +import scala.reflect.macros.whitebox.Context +import scala.language.experimental.macros +import scala.annotation.StaticAnnotation +import scala.annotation.compileTimeOnly + +class DebugTransforms(val c: Context) { + import c.universe._ + + /** Passthrough transform that prints the annottee for debugging purposes. + * No guarantees are made on what this annotation does, and it may very well change over time. + * + * The print is warning level to make it visually easier to spot, as well as a reminder that + * this annotation should not make it to production / committed code. + */ + def dump(annottees: c.Tree*): c.Tree = { + val combined = annottees.map({ tree => show(tree) }).mkString("\r\n\r\n") + annottees.foreach(tree => c.warning(c.enclosingPosition, s"Debug dump:\n$combined")) + q"..$annottees" + } + + /** Passthrough transform that prints the annottee as a tree for debugging purposes. + * No guarantees are made on what this annotation does, and it may very well change over time. + * + * The print is warning level to make it visually easier to spot, as well as a reminder that + * this annotation should not make it to production / committed code. + */ + def treedump(annottees: c.Tree*): c.Tree = { + val combined = annottees.map({ tree => showRaw(tree) }).mkString("\r\n") + annottees.foreach(tree => c.warning(c.enclosingPosition, s"Debug tree dump:\n$combined")) + q"..$annottees" + } +} + +class NamingTransforms(val c: Context) { + import c.universe._ + import Flag._ + + val globalNamingStack = q"_root_.chisel3.internal.DynamicNamingStack()" + + /** Base transformer that provides the val name transform. + * Should not be instantiated, since by default this will recurse everywhere and break the + * naming context variable bounds. + */ + trait ValNameTransformer extends Transformer { + val contextVar: TermName + + override def transform(tree: Tree) = tree match { + // Intentionally not prefixed with $mods, since modifiers usually mean the val definition + // is in a non-transformable location, like as a parameter list. + // TODO: is this exhaustive / correct in all cases? + case q"val $tname: $tpt = $expr" => { + val TermName(tnameStr: String) = tname + val transformedExpr = super.transform(expr) + q"val $tname: $tpt = $contextVar.name($transformedExpr, $tnameStr)" + } + case other => super.transform(other) + } + } + + /** Module-specific val name transform, containing logic to prevent from recursing into inner + * classes and applies the naming transform on inner functions. + */ + class ModuleTransformer(val contextVar: TermName) extends ValNameTransformer { + override def transform(tree: Tree) = tree match { + case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => + tree // don't recurse into inner classes + case q"$mods trait $tpname[..$tparams] extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => + tree // don't recurse into inner classes + case q"$mods def $tname[..$tparams](...$paramss): $tpt = $expr" => { + val Modifiers(_, _, annotations) = mods + val containsChiselName = annotations.map({q"new chiselName()" equalsStructure _}).fold(false)({_||_}) + if (containsChiselName) { + tree // don't apply the transform multiple times + } else { + // apply chiselName transform by default + val transformedExpr = transformHierarchicalMethod(expr) + q"$mods def $tname[..$tparams](...$paramss): $tpt = $transformedExpr" + } + } + case other => super.transform(other) + } + } + + /** Method-specific val name transform, handling the return case. + */ + class MethodTransformer(val contextVar: TermName) extends ValNameTransformer { + override def transform(tree: Tree) = tree match { + // TODO: better error messages when returning nothing + case q"return $expr" => q"return $globalNamingStack.pop_return_context($expr, $contextVar)" + // Do not recurse into methods + case q"$mods def $tname[..$tparams](...$paramss): $tpt = $expr" => tree + case other => super.transform(other) + } + } + + /** Applies the val name transform to a module body. Pretty straightforward, since Module is + * the naming top level. + */ + def transformModuleBody(stats: List[c.Tree]) = { + val contextVar = TermName(c.freshName("namingContext")) + val transformedBody = (new ModuleTransformer(contextVar)).transformTrees(stats) + + q""" + val $contextVar = $globalNamingStack.push_context() + ..$transformedBody + $contextVar.name_prefix("") + $globalNamingStack.pop_context($contextVar) + """ + } + + /** Applies the val name transform to a method body, doing additional bookkeeping with the + * context to allow names to propagate and prefix through the function call stack. + */ + def transformHierarchicalMethod(expr: c.Tree) = { + val contextVar = TermName(c.freshName("namingContext")) + val transformedBody = (new MethodTransformer(contextVar)).transform(expr) + + q"""{ + val $contextVar = $globalNamingStack.push_context() + $globalNamingStack.pop_return_context($transformedBody, $contextVar) + } + """ + } + + /** Applies naming transforms to vals in the annotated module or method. + * + * For methods, a hierarchical naming transform is used, where it will try to give objects names + * based on the call stack, assuming all functions on the stack are annotated as such and return + * a non-AnyVal object. Does not recurse into inner functions. + * + * For modules, this serves as the root of the call stack hierarchy for naming purposes. Methods + * will have chiselName annotations (non-recursively), but this does NOT affect inner classes. + * + * Basically rewrites all instances of: + * val name = expr + * to: + * val name = context.name(expr, name) + */ + def chiselName(annottees: c.Tree*): c.Tree = { + var namedElts: Int = 0 + + val transformed = annottees.map(annottee => annottee match { + case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" => { + val transformedStats = transformModuleBody(stats) + namedElts += 1 + q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$transformedStats }" + } + case q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }" => { + annottee // Don't fail noisly when a companion object is passed in with the actual class def + } + // Currently disallow on traits, this won't work well with inheritance. + case q"$mods def $tname[..$tparams](...$paramss): $tpt = $expr" => { + val transformedExpr = transformHierarchicalMethod(expr) + namedElts += 1 + q"$mods def $tname[..$tparams](...$paramss): $tpt = $transformedExpr" + } + case other => c.abort(c.enclosingPosition, s"@chiselName annotion may only be used on classes and methods, got ${showCode(other)}") + }) + + if (namedElts != 1) { + c.abort(c.enclosingPosition, s"@chiselName annotation did not match exactly one valid tree, got:\r\n${annottees.map(tree => showCode(tree)).mkString("\r\n\r\n")}") + } + + q"..$transformed" + } +} diff --git a/src/main/scala/chisel3/compatibility.scala b/src/main/scala/chisel3/compatibility.scala index abbe8ffe..a919e3b9 100644 --- a/src/main/scala/chisel3/compatibility.scala +++ b/src/main/scala/chisel3/compatibility.scala @@ -6,6 +6,10 @@ package object Chisel { // scalastyle:ignore package.object.name import chisel3.internal.firrtl.Width + import scala.language.experimental.macros + import scala.annotation.StaticAnnotation + import scala.annotation.compileTimeOnly + implicit val defaultCompileOptions = chisel3.core.ExplicitCompileOptions.NotStrict type Direction = chisel3.core.Direction @@ -333,4 +337,27 @@ package object Chisel { // scalastyle:ignore package.object.name val Pipe = chisel3.util.Pipe type Pipe[T <: Data] = chisel3.util.Pipe[T] + + /** Package for experimental features, which may have their API changed, be removed, etc. + * + * Because its contents won't necessarily have the same level of stability and support as + * non-experimental, you must explicitly import this package to use its contents. + */ + object experimental { + import scala.annotation.StaticAnnotation + import scala.annotation.compileTimeOnly + + @compileTimeOnly("enable macro paradise to expand macro annotations") + class dump extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro chisel3.internal.naming.DebugTransforms.dump + } + @compileTimeOnly("enable macro paradise to expand macro annotations") + class treedump extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro chisel3.internal.naming.DebugTransforms.treedump + } + @compileTimeOnly("enable macro paradise to expand macro annotations") + class chiselName extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro chisel3.internal.naming.NamingTransforms.chiselName + } + } } diff --git a/src/main/scala/chisel3/package.scala b/src/main/scala/chisel3/package.scala index cba8dffe..109bd14e 100644 --- a/src/main/scala/chisel3/package.scala +++ b/src/main/scala/chisel3/package.scala @@ -285,5 +285,22 @@ package object chisel3 { // scalastyle:ignore package.object.name */ def range(args: Any*): (NumericBound[Int], NumericBound[Int]) = macro chisel3.internal.RangeTransform.apply } + + import scala.language.experimental.macros + import scala.annotation.StaticAnnotation + import scala.annotation.compileTimeOnly + + @compileTimeOnly("enable macro paradise to expand macro annotations") + class dump extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro chisel3.internal.naming.DebugTransforms.dump + } + @compileTimeOnly("enable macro paradise to expand macro annotations") + class treedump extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro chisel3.internal.naming.DebugTransforms.treedump + } + @compileTimeOnly("enable macro paradise to expand macro annotations") + class chiselName extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro chisel3.internal.naming.NamingTransforms.chiselName + } } } diff --git a/src/test/scala/chiselTests/NamingAnnotationTest.scala b/src/test/scala/chiselTests/NamingAnnotationTest.scala new file mode 100644 index 00000000..7b05d338 --- /dev/null +++ b/src/test/scala/chiselTests/NamingAnnotationTest.scala @@ -0,0 +1,200 @@ +// See LICENSE for license details. + +package chiselTests + +import chisel3._ +import chisel3.experimental.{chiselName, dump} +import org.scalatest._ +import org.scalatest.prop._ +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 expectedNameMap = ListBuffer[(Data, String)]() + + /** Expects some name for a node that is propagated to FIRRTL. + * The node is returned allowing this to be called inline. + */ + def expectName[T <: Data](node: T, fullName: String): T = { + expectedNameMap += ((node, fullName)) + node + } + + /** After this module has been elaborated, returns a list of (node, expected name, actual name) + * that did not match expectations. + * Returns an empty list if everything was fine. + */ + def getNameFailures(): List[(Data, String, String)] = { + val failures = ListBuffer[(Data, String, String)]() + for ((ref, expectedName) <- expectedNameMap) { + if (ref.instanceName != expectedName) { + failures += ((ref, expectedName, ref.instanceName)) + } + } + failures.toList + } +} + +@chiselName +class NamedModule extends NamedModuleTester { + @chiselName + def FunctionMockupInner(): UInt = { + val my2A = 1.U + val my2B = expectName(my2A +& 2.U, "test_myNested_my2B") + val my2C = my2B +& 3.U // should get named at enclosing scope + my2C + } + + @chiselName + def FunctionMockup(): UInt = { + val myNested = expectName(FunctionMockupInner(), "test_myNested") + val myA = expectName(1.U + myNested, "test_myA") + val myB = expectName(myA +& 2.U, "test_myB") + val myC = expectName(myB +& 3.U, "test_myC") + myC +& 4.U // named at enclosing scope + } + + // chiselName "implicitly" applied + def ImplicitlyNamed(): UInt = { + val implicitA = expectName(1.U + 2.U, "test3_implicitA") + val implicitB = expectName(implicitA + 3.U, "test3_implicitB") + implicitB + 2.U // named at enclosing scope + } + + // Ensure this applies a partial name if there is no return value + def NoReturnFunction() { + val noreturn = expectName(1.U + 2.U, "noreturn") + } + + + val test = expectName(FunctionMockup(), "test") + val test2 = expectName(test +& 2.U, "test2") + val test3 = expectName(ImplicitlyNamed(), "test3") + + // Test that contents of for loops are named + for (i <- 0 until 1) { + val forInner = expectName(test3 + i.U, "forInner") + } + + // Test that contents of anonymous functions are named + Seq((0, "anonInner"), (1, "anonInner_1"), (2, "anonInner_2")).foreach { case (in, name) => + val anonInner = expectName(test3 + in.U, name) + } + + NoReturnFunction() +} + +@chiselName +class NameCollisionModule extends NamedModuleTester { + @chiselName + def repeatedCalls(id: Int): UInt = { + val test = expectName(1.U + 3.U, s"test_$id") // should disambiguate by invocation order + test + 2.U + } + + // chiselName applied by default to this + def innerNamedFunction() { + // ... but not this inner function + def innerUnnamedFunction() { + val a = repeatedCalls(1) + val b = repeatedCalls(2) + } + + innerUnnamedFunction() + } + + val test = expectName(1.U + 2.U, "test") + innerNamedFunction() +} + +/** Ensure no crash happens if a named function is enclosed in a non-named module + */ +class NonNamedModule extends NamedModuleTester { + @chiselName + def NamedFunction(): UInt = { + val myVal = 1.U + 2.U + myVal + } + + val test = NamedFunction() +} + +/** Ensure no crash happens if a named function is enclosed in a non-named function in a named + * module. + */ +object NonNamedHelper { + @chiselName + def NamedFunction(): UInt = { + val myVal = 1.U + 2.U + myVal + } + + def NonNamedFunction() : UInt = { + val myVal = NamedFunction() + myVal + } +} + +@chiselName +class NonNamedFunction extends NamedModuleTester { + val test = NonNamedHelper.NamedFunction() +} + +/** Ensure broken links in the chain are simply dropped + */ +@chiselName +class PartialNamedModule extends NamedModuleTester { + // Create an inner function that is the extent of the implicit naming + def innerNamedFunction(): UInt = { + def innerUnnamedFunction(): UInt = { + @chiselName + def disconnectedNamedFunction(): UInt = { + val a = expectName(1.U + 2.U, "test_a") + val b = expectName(a + 2.U, "test_b") + b + } + disconnectedNamedFunction() + } + innerUnnamedFunction() + 1.U + } + + val test = innerNamedFunction() +} + + +/** A simple test that checks the recursive function val naming annotation both compiles and + * generates the expected names. + */ +class NamingAnnotationSpec extends ChiselPropSpec { + property("NamedModule should have function hierarchical names") { + // TODO: clean up test style + var module: NamedModule = null + elaborate { module = new NamedModule; module } + assert(module.getNameFailures() == Nil) + } + + property("NameCollisionModule should disambiguate collisions") { + // TODO: clean up test style + var module: NameCollisionModule = null + elaborate { module = new NameCollisionModule; module } + assert(module.getNameFailures() == Nil) + } + + property("PartialNamedModule should have partial names") { + // TODO: clean up test style + var module: PartialNamedModule = null + elaborate { module = new PartialNamedModule; module } + assert(module.getNameFailures() == Nil) + } + + property("NonNamedModule should elaborate") { + elaborate { new NonNamedModule } + } + + property("NonNamedFunction should elaborate") { + elaborate { new NonNamedFunction } + } +} |
