I consider ALUs (Arithmetic logic units) to be interesting because they are a direct application of Boolean algebra, and it is easy to extend into something realistically resembling a computer (from the 1980s). This post is going to be about sketching a small ALU implementation in Haskell. I originally wrote this code for a discussion section for a Discrete Math honors course, presented right after they learned Boolean algebra.
I have since cleaned the code up once or twice, and I decided it would be neat to present it here. Due to its length, I am going to do it in a few parts:
- We will construct a simple ALU Adder for 32-bit Boolean values
Next, we will construct a larger ALU to support
and
,or
, andxor
and then construct a multiplexer to give us the correct answer. - The second installment will also feature the addition of registers
with
load
andsave
to feature more general computation. After, that we will add program counter, branching and jumps to construct something resembling a full computer.
In this first installment, we begin simply with the classical
definition of xor
, which will allow us to circumvent the classical
construction of a
half-bit adder,
We also include an infix version, *|
, for ease of use. This
implementation should be straight-forward; we simply dispatch on the
input and produce the appropriate truth value.
We are now ready to construct a whole-bit adder from two half-bit adders with a bit of “glue” to connect the three inputs with the two outputs. I would refer the interested reader to this page to see how this works with circuitry; the idea is that we add the three bits through two half-bit adders, and then do an appropriate amount of math to construct the carry-out bit (that is, the carry that results from the addition):
We are almost ready to construct our ALU, but we have one last detail: we need some mechanism to encode and decode natural numbers to binary, representing a number a as a list of Booleans of length 32:
Next, we construct our entire ALU in one “fell swoop”, building a
simple
ripple-carry adder,
to sum two numbers. This implementation will take both inputs as lists
of Boolean values and iterate over them, rippling the carry bit
through to compute the binary addition in terms of bac
.
This ALU starts with an empty carry-in and iteratively performs bac
over each pair of inpus, threading the new carry bit through to the
next computation until we are at the end. Notice that we throw the
final carry bit away; overflowing is just find on a processor.
Even so, we have now constructed a 32-bit unsigned integer adder
ALU from first principles: from and
and or
, or, perhaps
more accurately, and
, or
, and xor
. This ALU isn’t particularly
useful right now, but extending it to feature a multiplexer is
straight-forward. We will “cheat” here for simplicity, however.
Constructing a full multiplexer from and
and or
requires a series
of additional analog circuitry:
This construction is left as an exercise to the reader; instead, we will opt for the following digital multiplexer, and define our operations in terms of it.
The output of our ALU now constructs a list of lists, where each inner list is
the result of each operation on the appropriate bits, where the first is
addition, the second is and
, the third is or
, and the fourth is xor
. We
select each one out, and construct that result as our final computational
result for the operator. This requires a modification to our ALU, however, to
return this shape in every step:
Then our final ALU uses the multiplexer on the result of this bit ALU to build the final computational ALU we will use:
We now have something resembling a computer! But it is not enough to make this claim, so we will demonstrate that it is the case. To this end, we construct a small language for our ALU, with a simple test case:
Finally, we build a small driver that peforms these programs, using our previous definition of operations to look up and dispatch to the multiplexer appropriately:
The output is 14, which, it turns out, is the result of (2 + 4) && 6 || 8
(unsurprisingly). We now have a basic language—more of a calculator, but
still! We can perform basic Boolean logic operations over our input, which
lives in the “operator” register. But still, we’ve constructed the basics
of computation out of and
, or
, xor
, and small operator dispatch using
a multiplexer, and built a language on top of it.
In our next installment, we will extend our language and framework with full registers and then branching operators, constructing a small instruction set for general computation on top of this ALU.