Bus-level design

Customizing Scala collections

Now that we have data and control put into good use, let us approach the next issue in terms of design convenience. Namely, the transition from designing with individual wires that carry bits (that is, individual gates in our Scala simulation) to designing with buses of wires that carry words (that is, sequences of gates in our Scala simulation). Recall from Round 2: Bits and data that a word is the atomic unit by which a processor architecture manipulates data. The word length of the architecture is the number of bits in a word.

The necessity to design at the level of words is of course an excellent motivation to drill into the Scala collections framework, and in particular into the tools to manipulate sequences. If possible, we want our bus-level designs to be just as succinct as our gate-level designs. Indeed, recall that our goal for this round is to build a Boolean circuit that adds two 64-bit integers.

Let us start by recalling our gate-level design for selecting between two gates:

val r = (a && !s) || (b && s)

Here a and b were the data inputs (individual gates), and s was the control input (individual gate).

_images/mux2-dnf.png

We want to upgrade the gate-selector to a bus-selector, that selects between two n-bit buses. Here we display a bus-selector that selects between two 4-bit buses, aa and bb, to produce the 4-bit bus rr as output.

_images/mux2-dnf-bus.png

The circuit diagram may look a bit daunting at first, but observe that this is in fact not the case. The same two-input selector is simply repeated four times, once for each pair of corresponding bits aa(i) and bb(i) in the two buses. In particular, the same control input s drives all four selectors.

To enter the realm of bus-level design, let us first try to work with the obvious abstraction, namely let us just work with an array of gates Array[Gate] whenever a bus is needed.

Say, let us transport the gate-level design into a design with two n-bit buses with n = 4. Let us build these buses out of input elements:

val n  = 4
val aa = new Array[Gate](n)
for i <- 0 until n do aa(i) = new InputElement()
val bb = new Array[Gate](n)
for i <- 0 until n do bb(i) = new InputElement()
val s  = new InputElement()

Here is a first attempt to build a two-bus selector that uses a for-loop over the bits of the bus:

val rr_attempt1 = new Array[Gate](n)
for i <- 0 until n do
  rr_attempt1(i) = (aa(i) && !s)||(bb(i) && s)
end for

This code will build precisely the circuit depicted above, so in principle we are done.

But again we should be more critical. Indeed, we are using a for-loop when far more succinct and idiomatic expressions exist for the task. Also the need to reserve new Array-objects looks cumbersome.

Here are three further attempts, throwing in some skill at using Scala collections and anonymous functions. (Do not worry if you cannot follow in detail what these one-liners do, we will get practice during later rounds.) Here we go:

val rr_attempt2 = (aa.map(_ && !s) zip bb.map(_ && s)).map((p,q) => p || q)

Here is another go:

val rr_attempt3 = (aa zip bb).map((a,b) => (a && !s) || (b && s))

And yet another:

val rr_attempt4 = (aa zip bb).map(x => (x._1 && !s) || (x._2 && s))

A quick Toggler session gives us a fair amount of confidence that all four solutions work (try this!):

val t = new Toggler()
t.watch("aa", aa)
t.watch("bb", bb)
t.watch("s", s)
t.watch("rr1", rr_attempt1)
t.watch("rr2", rr_attempt2)
t.watch("rr3", rr_attempt3)
t.watch("rr4", rr_attempt4)
t.go()

Several working solutions are a fair start, but we should not be happy, since the Scala code is far from succinct and readable.

So what is it that we would like? An appropriately apt solution would perhaps be:

val rr = (aa && !s)|(bb && s)

And while we are at it, perhaps the setup code could be also cleaned. If we place some routine setup code into aptly named companion objects, the entire two-bus selector gets built with:

val n  = 4
val aa = Bus.inputs(n)
val bb = Bus.inputs(n)
val s  = Gate.input()
val rr = (aa && !s)|(bb && s)

Incidentally, the bus-level design tools are already implemented in package tinylog. Just copy and paste what is above into your console after include tinylog.* to witness this.

The package tinylog is included with the exercises for this round. If you open it up you will see something like in the listing below. Understanding the entire code is not necessary at this point, but note that Bus extends Seq[Gate], that is, a bus is a sequence of Gate objects. This allows us to perform standard sequence operations, such as zip on Bus as well as the defined custom methods.

package tinylog
import scala.collection.{SpecificIterableFactory, StrictOptimizedSeqOps, mutable}
import collection.SeqOps

sealed class Bus(gates: Seq[Gate]) extends Seq[Gate]
    with SeqOps[Gate, Seq, Bus]
    with StrictOptimizedSeqOps[Gate, Seq, Bus]:

  // Mandatory implementation of `apply` in SeqOps
  def apply(idx: Int) = gates.apply(idx)

  /** Creates a new Bus from a set of indexes to this one.*/
  def apply(idxs: Seq[Int]) = new Bus(idxs.map(gates(_)))

  // Mandatory implementation of `length` and `iterator`
  def length = gates.length
  def iterator = gates.iterator

  /* Operations on Gates.*/

  /** Values of Gates.*/
  def values = gates.map(_.value)

  /** The number of gates (i) in this bus and (ii) recursively referenced by the ones in this bus. */
  def nofGates: Int =
    val counted = new mutable.HashSet[Gate]()
    gates.foldLeft(0)((result, gate) => result + gate.nofReferenced(counted))

  /**
   * For a bus aa and gate g, aa && g returns a new bus cc
   * of length aa.length such that cc(i) is aa(i) && g.
   */
  def &&(that: Gate) = new Bus(this.map(_ && that))

  /**
   * For a bus aa and gate g, aa || g returns a new bus cc
   * of length aa.length such that cc(i) is aa(i) || g.
   */
  def ||(that: Gate) = new Bus(this.map(_ || that))

  /**
   * Bitwise negation of the bus.
   * For a bus aa, ~aa is a new bus cc such that cc(i) is !aa(i).
   */
  def unary_~ = this.map(!_)

  /**
   * Bitwise AND of two busses.
   * For two busses aa and bb, aa & bb returns a new bus cc
   * of length aa.length such that cc(i) is aa(i) && bb(i).
   * The busses must be of the same length.
   */
  def &(that: Bus) =
    require(this.length == that.length, "Cannot take bitwise and of busses of different length")
    new Bus((this zip that).map(x => x._1 && x._2))

  /**
   * Bitwise OR of two busses.
   * For two busses aa and bb, aa | bb returns a new bus cc
   * of length aa.length such that cc(i) is aa(i) || bb(i).
   * The busses must be of the same length.
   */
  def |(that: Bus) =
    require(this.length == that.length, "Cannot take bitwise and of busses of different length")
    new Bus((this zip that).map(x => x._1 || x._2))
  /* Because Bus is a custom collection (based on Seq) with SeqOps trait
     we need to override a a few methods so that it can inherit all of the
     standard operations from the trait while still behaving as a Bus as
     much as possible.
     If you are interested, see the RNA exampe at 
     https://docs.scala-lang.org/overviews/core/custom-collections.html#final-version-of-rna-strands-class
   */

  // Mandatory overrides of `fromSpecific`, `newSpecificBuilder`,
  // and `empty`, from `IterableOps`
  override protected def fromSpecific(coll: IterableOnce[Gate]): Bus =
    Bus.fromSpecific(coll)

  override protected def newSpecificBuilder: mutable.Builder[Gate, Bus] =
    Bus.newBuilder

  override def empty: Bus = Bus.empty

  // Overloading of `appended`, `prepended`, `appendedAll`, `prependedAll`,
  // `map`, `flatMap` and `concat` to return a `Bus` when possible
  def concat(suffix: IterableOnce[Gate]): Bus =
    strictOptimizedConcat(suffix, newSpecificBuilder)

  @inline final def ++ (suffix: IterableOnce[Gate]): Bus = concat(suffix)

  def appended(base: Gate): Bus =
    (newSpecificBuilder ++= this += base).result()

  def appendedAll(suffix: Iterable[Gate]): Bus =
    strictOptimizedConcat(suffix, newSpecificBuilder)

  def prepended(base: Gate): Bus =
    (newSpecificBuilder += base ++= this).result()

  def prependedAll(prefix: Iterable[Gate]): Bus =
    (newSpecificBuilder ++= prefix ++= this).result()

  def map(f: Gate => Gate): Bus =
    strictOptimizedMap(newSpecificBuilder, f)

  def flatMap(f: Gate => IterableOnce[Gate]): Bus =
    strictOptimizedFlatMap(newSpecificBuilder, f)

  // The class name will by default be shown as 'Seq', we don't want that.
  override def className = "Bus"


object Bus extends SpecificIterableFactory[Gate, Bus]:

  def empty: Bus = new Bus(Seq.empty)

  def newBuilder: mutable.Builder[Gate,Bus] =
    mutable.ArrayBuffer.newBuilder[Gate]
    .mapResult(s=>new Bus(s.toSeq))

  def fromSpecific(it: IterableOnce[Gate]): Bus = it match
    case seq: Seq[Gate] => new Bus(seq)
    case _ => new Bus(it.iterator.toSeq)

  /** Returns a new bus with n InputElement gates */
  def inputs(n: Int) = new Bus((1 to n).map(x => Gate.input()))
  /** Returns a new bus of n False gates */
  def falses(n: Int) = new Bus((1 to n).map(x => Gate.False))
  /** Returns a new bus of n True gates */
  def trues(n: Int) = new Bus((1 to n).map(x => Gate.True))

What sets a Bus apart from a plain sequence of objects of type Gate is that there are custom methods (operator definitions) inside class Bus that enable succinct bus-level building:

def &&(that: Gate) = new Bus(this.map(_ && that))
def ||(that: Gate) = new Bus(this.map(_ || that))
def unary_~        = this.map(!_)
def &(that: Bus)   = new Bus((this zip that).map(x => x._1 && x._2))
def |(that: Bus)   = new Bus((this zip that).map(x => x._1 || x._2))

Let us take a more detailed look at these operators. (At this point it may be convenient to recall how we defined the gate-level operators in the previous section.)

First, let us look at the operators

def &&(that: Gate) = new Bus(this.map(_ && that))
def ||(that: Gate) = new Bus(this.map(_ || that))

Observe that the operators && and || are defined so that the left operand (this) is a Bus, and the right operand (that) is a Gate. In both cases the result is a new bus, consisting of AND and OR gates (depending on whether && or || was applied) such that the left input is a gate from the bus, and the right input is the right operand (the gate that). (The expression _ && that is a shorthand for the function z => z && that.) If you are familiar with vectors and scalars, this is the circuit-building analogue of operating on a vector with a scalar.

The following figure illustrates the bus that gets built when we execute the expression bb && g for a 4-gate bus bb and a gate g.

_images/bb-and-g.png

Let us proceed to the next operator:

def unary_~        = this.map(!_)

The operator ~ builds, one entry of its operand (this) at a time, a NOT-gate that takes input from the entry (the gate of the bus), and returns a bus consisting of the NOT-gates built. This is, in essence, the circuit-building analogue of vector negation.

The following figure illustrates the bus that gets built when we execute the expression ~bb for a 4-gate bus bb.

_images/not-bb.png

Let us then look at the remaining operators:

def &(that: Bus)   = new Bus((this zip that).map(x => x._1 && x._2))
def |(that: Bus)   = new Bus((this zip that).map(x => x._1 || x._2))

In this case both operands (this and that) are buses. The operator & (respectively, the operator |) builds, one entry at a time, an AND-gate (OR-gate) that takes input from the corresponding entries (gates) in the left and right operands, and return a bus consisting of the AND-gates (OR-gates) built. This is the circuit-building analogue of vector addition.

The following figure illustrates the bus that gets built when we execute the expression aa | bb for two 4-gate buses aa and bb.

_images/aa-or-bb.png

Combining the bus-level operators, we observe that executing (aa && !s)|(bb && s) indeed constructs the desired selector:

_images/mux2-dnf-bus-final.png

Convention on indexing the bits in a bus

When drawing circuits, it is customary to index the bits in a bus from right to left. That is, the bit with index 0 is the rightmost bit in a bus, and the indices increase from right to left, from the least significant bit to the most significant bit.

_images/aa-or-bb-idx.png

This convention makes it easy to work with buses that carry binary integers, because the integer is displayed on the bus directly in its customary binary form, with the most significant binary digit (bit) on the left, and the least significant binary digit on the right. (Recall our discussion of the positional number system on Round 2: Bits and data.)

Functional design – encapsulating code that builds subcircuits

To arrive at more intricate circuit designs, it is useful to encapsulate circuit-building code into procedures that hide the implementation and enable us to build larger circuits out of smaller component circuits. For example, our two-bus selector can be encapsulated as:

def buildTwoBusSelector(aa: Bus, bb: Bus, s: Gate) =
  (aa && !s)|(bb && s)

Here is a builder that reverses the order of bits in a bus:

def buildReverser(aa: Bus) =
  aa.reverse

Observe that a reverser actually builds no gates, but rather it simply reverses the order of bits in a bus. We display a reverser for word length 8 below:

_images/aa-reverser.png

Here is a builder that shifts in k zero bits from the right, and drops k bits out from the left:

def buildLeftShift(aa: Bus, k: Int) =
  Bus.falses(k) ++ aa.dropRight(k)

We display a left shift for k = 3 and word length 8 below:

_images/aa-leftshift.png

Of course the power of such an approach is revealed when we start combining builders to yield more complex builders. Here is a builder for a circuit that outputs either its input aa, or aa reversed, depending on the value of a control input s:

def buildIdentityOrReverse(aa: Bus, s: Gate) =
  buildTwoBusSelector(aa, buildReverser(aa), s)

Observe how all the attention that we have put into making our tools convenient to use is now starting to pay off.

Here is a shifter that shifts to the left by 1 or 3, depending on the control input s:

def buildLeftShift1Or3(aa: Bus, s: Gate) =
  buildTwoBusSelector(buildLeftShift(aa,1),
                      buildLeftShift(aa,3),
                      s)

What about selecting among more than two possibilities?

Here is a builder for a selector (or multiplexer) that takes a sequence of input buses and a sequence (bus) of selector bits, and outputs the bus that corresponds to the most significant selector bit that is set to true, or an all-false bus if all selector bits are false:

def buildBusSelector(buses: Seq[Bus], selectors: Bus) =
  var rr = Bus.falses(buses(0).length)
  for i <- 0 until buses.length do
    val bb = buses(i)
    val s = selectors(i)
    rr = buildTwoBusSelector(rr,bb,s)
  end for
  rr
end buildBusSelector

Here is the same builder but written in a different style that makes use of the foldLeft operation and a case-expression to match the components of a tuple:

def buildBusSelector(buses: Seq[Bus], selectors: Bus) =
 (buses zip selectors).foldLeft(Bus.falses(buses(0).length)){
    case (rr,(bb,s)) => buildTwoBusSelector(rr,bb,s)
  }
end buildBusSelector

Let us now combine our tools for a bit of play. Here is a design for Toggler that shifts the 16-bit input by any amount of your choosing between 0 and 15 bits to the left:

val n             = 16
val input         = Bus.inputs(n)
val amount_select = Bus.inputs(n)
val shifts        = (0 until n).map(k => buildLeftShift(input, k))
val output        = buildBusSelector(shifts, amount_select)

val t = new Toggler()
  // reversion will display least significant bit on the right
t.watch("input",         input.reverse)
t.watch("amount_select", amount_select.reverse)
t.watch("output",        output.reverse)
t.go()

All right, that was fun. But suppose we wanted to give the selection as a binary input? That is, \(b\) bits suffice to select among \(2^{b}\) choices.

Here is a builder for a binary decoder circuit that takes an integer \(j = 0, 1, \ldots, 2^{b} - 1\) as a \(b\)-bit bus of input and gives a \(2^{b}\)-bit bus of output such that on input \(j\) exactly the output with index \(j\) is true.

def buildDecoder(aa: Bus) =
  aa.foldLeft(Bus(Gate.True))((d,a) => (d && !a)++(d && a))

To observe a binary decoder in operation with Toggler, copy and paste the following code to the console, toggle in any 4-bit binary number via the inputs, and witness exactly the output indexed by that number become true among the 16-bit output:

val amount_bits = 4
val input       = Bus.inputs(amount_bits)
val output      = buildDecoder(input)

val t = new Toggler()
  // reversion will display least significant bit on the right
t.watch("input",  input.reverse)
t.watch("output", output.reverse)
t.go()

Here is the previous shifter code, but now using a binary decoder so that the shift amount can be given in binary using 4 bits:

val n           = 16
val amount_bits = 4  // the base-2 log of n
val input       = Bus.inputs(n)
val amount      = Bus.inputs(amount_bits)
val shifts      = (0 until n).map(k => buildLeftShift(input, k))
val output      = buildBusSelector(shifts, buildDecoder(amount))

val t = new Toggler()
  // reversion will display least significant bit on the right
t.watch("input",  input.reverse)
t.watch("amount", amount.reverse)
t.watch("output", output.reverse)
t.go()

Remark. At this point we should observe and recognize the fruits of our efforts at making our tools convenient. With a few lines of code, we can build and play with intricate circuit designs that accept buses of data input and control input. Think about how many lines of code it would require to build these circuits out of individual gates if we had not paid attention to developing our tools to match the increased complexity.

With our tools sharpened to deal with bus-level designs, we are ready now ready to meet our goal for this round.