diff options
| -rw-r--r-- | chiselFrontend/src/main/scala/chisel3/core/Bits.scala | 17 | ||||
| -rw-r--r-- | chiselFrontend/src/main/scala/chisel3/internal/firrtl/IR.scala | 48 | ||||
| -rw-r--r-- | coreMacros/src/main/scala/chisel3/internal/RangeTransform.scala | 102 | ||||
| -rw-r--r-- | src/main/scala/chisel3/package.scala | 14 | ||||
| -rw-r--r-- | src/test/scala/chiselTests/RangeSpec.scala | 104 |
5 files changed, 284 insertions, 1 deletions
diff --git a/chiselFrontend/src/main/scala/chisel3/core/Bits.scala b/chiselFrontend/src/main/scala/chisel3/core/Bits.scala index 4a09c70e..82b60a4c 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Bits.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Bits.scala @@ -563,6 +563,14 @@ private[core] sealed trait UIntFactory { result.binding = LitBinding() result } + /** Create a UInt with the specified range */ + def apply(range: Range): UInt = { + width(range.getWidth) + } + /** Create a UInt with the specified range */ + def apply(range: (NumericBound[Int], NumericBound[Int])): UInt = { + apply(KnownUIntRange(range._1, range._2)) + } /** Create a UInt with a specified width - compatibility with Chisel2. */ // NOTE: This resolves UInt(width = 32) @@ -728,6 +736,15 @@ object SInt { /** Create an SInt literal with specified width. */ def apply(value: BigInt, width: Width): SInt = Lit(value, width) + /** Create a SInt with the specified range */ + def apply(range: Range): SInt = { + width(range.getWidth) + } + /** Create a SInt with the specified range */ + def apply(range: (NumericBound[Int], NumericBound[Int])): SInt = { + apply(KnownSIntRange(range._1, range._2)) + } + def Lit(value: BigInt): SInt = Lit(value, Width()) def Lit(value: BigInt, width: Int): SInt = Lit(value, Width(width)) /** Create an SInt literal with specified width. */ diff --git a/chiselFrontend/src/main/scala/chisel3/internal/firrtl/IR.scala b/chiselFrontend/src/main/scala/chisel3/internal/firrtl/IR.scala index 17b869f2..262b939f 100644 --- a/chiselFrontend/src/main/scala/chisel3/internal/firrtl/IR.scala +++ b/chiselFrontend/src/main/scala/chisel3/internal/firrtl/IR.scala @@ -109,6 +109,54 @@ case class Index(imm: Arg, value: Arg) extends Arg { override def fullName(ctx: Component): String = s"${imm.fullName(ctx)}[${value.fullName(ctx)}]" } +sealed trait Bound +sealed trait NumericBound[T] extends Bound { + val value: T +} +sealed case class Open[T](value: T) extends NumericBound[T] +sealed case class Closed[T](value: T) extends NumericBound[T] + +sealed trait Range { + val min: Bound + val max: Bound + def getWidth: Width +} + +sealed trait KnownIntRange extends Range { + val min: NumericBound[Int] + val max: NumericBound[Int] + + require( (min, max) match { + case (Open(low_val), Open(high_val)) => low_val < high_val - 1 + case (Closed(low_val), Open(high_val)) => low_val < high_val + case (Open(low_val), Closed(high_val)) => low_val < high_val + case (Closed(low_val), Closed(high_val)) => low_val <= high_val + }) +} + +sealed case class KnownUIntRange(min: NumericBound[Int], max: NumericBound[Int]) extends KnownIntRange { + require (min.value >= 0) + + def getWidth: Width = max match { + case Open(v) => Width(BigInt(v - 1).bitLength.max(1)) + case Closed(v) => Width(BigInt(v).bitLength.max(1)) + } +} + +sealed case class KnownSIntRange(min: NumericBound[Int], max: NumericBound[Int]) extends KnownIntRange { + + val maxWidth = max match { + case Open(v) => Width(BigInt(v - 1).bitLength + 1) + case Closed(v) => Width(BigInt(v).bitLength + 1) + } + val minWidth = min match { + case Open(v) => Width(BigInt(v + 1).bitLength + 1) + case Closed(v) => Width(BigInt(v).bitLength + 1) + } + def getWidth: Width = maxWidth.max(minWidth) + +} + object Width { def apply(x: Int): Width = KnownWidth(x) def apply(): Width = UnknownWidth() diff --git a/coreMacros/src/main/scala/chisel3/internal/RangeTransform.scala b/coreMacros/src/main/scala/chisel3/internal/RangeTransform.scala new file mode 100644 index 00000000..f431341d --- /dev/null +++ b/coreMacros/src/main/scala/chisel3/internal/RangeTransform.scala @@ -0,0 +1,102 @@ +// See LICENSE for license details. + +// Macro transforms that statically (at compile time) parse range specifiers and emit the raw +// (non-human-friendly) range constructor calls. + +package chisel3.internal + +import scala.language.experimental.macros +import scala.reflect.macros.blackbox.Context +import scala.reflect.macros.whitebox + +class RangeTransform(val c: Context) { + import c.universe._ + def apply(args: c.Tree*): c.Tree = { + val stringTrees = c.prefix.tree match { + case q"$_(scala.StringContext.apply(..$strings))" => strings + case _ => c.abort(c.enclosingPosition, s"Range macro unable to parse StringContext, got: ${showCode(c.prefix.tree)}") + } + val strings = stringTrees.map { tree => tree match { + case Literal(Constant(string: String)) => string + case _ => c.abort(c.enclosingPosition, s"Range macro unable to parse StringContext element, got: ${showRaw(tree)}") + } } + + var nextStringIndex: Int = 1 + var nextArgIndex: Int = 0 + var currString: String = strings(0) + + /** Mutably gets the next numeric value in the range specifier. + */ + def getNextValue(): c.Tree = { + currString = currString.dropWhile(_ == ' ') // allow whitespace + if (currString.isEmpty) { + if (nextArgIndex >= args.length) { + c.abort(c.enclosingPosition, s"Incomplete range specifier") + } + val nextArg = args(nextArgIndex) + nextArgIndex += 1 + + if (nextStringIndex >= strings.length) { + c.abort(c.enclosingPosition, s"Incomplete range specifier") + } + currString = strings(nextStringIndex) + nextStringIndex += 1 + + nextArg + } else { + val nextStringVal = currString.takeWhile(!Set('[', '(', ' ', ',', ')', ']').contains(_)) + currString = currString.substring(nextStringVal.length) + if (currString.isEmpty) { + c.abort(c.enclosingPosition, s"Incomplete range specifier") + } + c.parse(nextStringVal) + } + } + + // Currently, not allowed to have the end stops (inclusive / exclusive) be interpolated. + currString = currString.dropWhile(_ == ' ') + val startInclusive = currString(0) match { + case '[' => true + case '(' => false + case other => c.abort(c.enclosingPosition, s"Unknown start inclusive/exclusive specifier, got: '$other'") + } + currString = currString.substring(1) // eat the inclusive/exclusive specifier + val minArg = getNextValue() + currString = currString.dropWhile(_ == ' ') + if (currString(0) != ',') { + c.abort(c.enclosingPosition, s"Incomplete range specifier, expected ','") + } + currString = currString.substring(1) // eat the comma + val maxArg = getNextValue() + currString = currString.dropWhile(_ == ' ') + val endInclusive = currString(0) match { + case ']' => true + case ')' => false + case other => c.abort(c.enclosingPosition, s"Unknown end inclusive/exclusive specifier, got: '$other'") + } + currString = currString.substring(1) // eat the inclusive/exclusive specifier + currString = currString.dropWhile(_ == ' ') + + if (nextArgIndex < args.length) { + val unused = args.mkString("") + c.abort(c.enclosingPosition, s"Unused interpolated values in range specifier: '$unused'") + } + if (!currString.isEmpty || nextStringIndex < strings.length) { + val unused = currString + strings.slice(nextStringIndex, strings.length).mkString(", ") + c.abort(c.enclosingPosition, s"Unused characters in range specifier: '$unused'") + } + + val startBound = if (startInclusive) { + q"_root_.chisel3.internal.firrtl.Closed($minArg)" + } else { + q"_root_.chisel3.internal.firrtl.Open($minArg)" + } + val endBound = if (endInclusive) { + q"_root_.chisel3.internal.firrtl.Closed($maxArg)" + } else { + q"_root_.chisel3.internal.firrtl.Open($maxArg)" + } + + q"($startBound, $endBound)" + } +} diff --git a/src/main/scala/chisel3/package.scala b/src/main/scala/chisel3/package.scala index 3cdda971..29aa6528 100644 --- a/src/main/scala/chisel3/package.scala +++ b/src/main/scala/chisel3/package.scala @@ -3,7 +3,7 @@ package object chisel3 { // scalastyle:ignore package.object.name import scala.language.experimental.macros - import internal.firrtl.Width + import internal.firrtl.{Width, NumericBound} import internal.sourceinfo.{SourceInfo, SourceInfoTransform} import util.BitPat @@ -202,5 +202,17 @@ package object chisel3 { // scalastyle:ignore package.object.name implicit def fromBigIntToIntParam(x: BigInt): IntParam = IntParam(x) implicit def fromDoubleToDoubleParam(x: Double): DoubleParam = DoubleParam(x) implicit def fromStringToStringParam(x: String): StringParam = StringParam(x) + + implicit class ChiselRange(val sc: StringContext) extends AnyVal { + /** Specifies a range using mathematical range notation. Variables can be interpolated using + * standard string interpolation syntax. + * @example {{{ + * UInt(range"[0, 2)") + * UInt(range"[0, $myInt)") + * UInt(range"[0, ${myInt + 2})") + * }}} + */ + def range(args: Any*): (NumericBound[Int], NumericBound[Int]) = macro chisel3.internal.RangeTransform.apply + } } } diff --git a/src/test/scala/chiselTests/RangeSpec.scala b/src/test/scala/chiselTests/RangeSpec.scala new file mode 100644 index 00000000..e2313f34 --- /dev/null +++ b/src/test/scala/chiselTests/RangeSpec.scala @@ -0,0 +1,104 @@ +// See LICENSE for license details. + +package chiselTests + +import chisel3._ +import chisel3.experimental.ChiselRange + +import chisel3.internal.firrtl.{Open, Closed} +import org.scalatest.{Matchers, FreeSpec} + +class RangeSpec extends FreeSpec with Matchers { + "Ranges can be specified for UInt, SInt, and FixedPoint" - { + "invalid range specifiers should fail at compile time" in { + assertDoesNotCompile(""" range"" """) + assertDoesNotCompile(""" range"[]" """) + assertDoesNotCompile(""" range"0" """) + assertDoesNotCompile(""" range"[0]" """) + assertDoesNotCompile(""" range"[0, 1" """) + assertDoesNotCompile(""" range"0, 1]" """) + assertDoesNotCompile(""" range"[0, 1, 2]" """) + assertDoesNotCompile(""" range"[a]" """) + assertDoesNotCompile(""" range"[a, b]" """) + assertCompiles(""" range"[0, 1]" """) // syntax sanity check + } + + "range macros should allow open and closed bounds" in { + range"[-1, 1)" should be( (Closed(-1), Open(1)) ) + range"[-1, 1]" should be( (Closed(-1), Closed(1)) ) + range"(-1, 1]" should be( (Open(-1), Closed(1)) ) + range"(-1, 1)" should be( (Open(-1), Open(1)) ) + } + + "range specifiers should be whitespace tolerant" in { + range"[-1,1)" should be( (Closed(-1), Open(1)) ) + range" [-1,1) " should be( (Closed(-1), Open(1)) ) + range" [ -1 , 1 ) " should be( (Closed(-1), Open(1)) ) + range" [ -1 , 1 ) " should be( (Closed(-1), Open(1)) ) + } + + "range macros should work with interpolated variables" in { + val a = 10 + val b = -3 + + range"[$b, $a)" should be( (Closed(b), Open(a)) ) + range"[${a + b}, $a)" should be( (Closed(a + b), Open(a)) ) + range"[${-3 - 7}, ${-3 + a})" should be( (Closed(-10), Open(-3 + a)) ) + + def number(n: Int): Int = n + range"[${number(1)}, ${number(3)})" should be( (Closed(1), Open(3)) ) + } + + "UInt should get the correct width from a range" in { + UInt(range"[0, 8)").getWidth should be (3) + UInt(range"[0, 8]").getWidth should be (4) + UInt(range"[0, 0]").getWidth should be (1) + } + + "SInt should get the correct width from a range" in { + SInt(range"[0, 8)").getWidth should be (4) + SInt(range"[0, 8]").getWidth should be (5) + SInt(range"[-4, 4)").getWidth should be (3) + SInt(range"[0, 0]").getWidth should be (1) + } + + "UInt should check that the range is valid" in { + an [IllegalArgumentException] should be thrownBy { + UInt(range"[1, 0]") + } + an [IllegalArgumentException] should be thrownBy { + UInt(range"[-1, 1]") + } + an [IllegalArgumentException] should be thrownBy { + UInt(range"(0,0]") + } + an [IllegalArgumentException] should be thrownBy { + UInt(range"[0,0)") + } + an [IllegalArgumentException] should be thrownBy { + UInt(range"(0,0)") + } + an [IllegalArgumentException] should be thrownBy { + UInt(range"(0,1)") + } + } + + "SInt should check that the range is valid" in { + an [IllegalArgumentException] should be thrownBy { + SInt(range"[1, 0]") + } + an [IllegalArgumentException] should be thrownBy { + SInt(range"(0,0]") + } + an [IllegalArgumentException] should be thrownBy { + SInt(range"[0,0)") + } + an [IllegalArgumentException] should be thrownBy { + SInt(range"(0,0)") + } + an [IllegalArgumentException] should be thrownBy { + SInt(range"(0,1)") + } + } + } +} |
