Professional Documents
Culture Documents
Simulating RAM in Clojure
Simulating RAM in Clojure
“Computers are all made out of logic gates”. We’ve heard that saying
before. We also have a sense that logic gates are very simple machines,
analogous to light switches even. This raises the question: how exactly do
kind-of-light-switches come together to form computers? How does “storing a
variable” or “calling a function” translate into logic gates going on or o ?
I liked his book so much that I took his schematic for RAM, and simulated
it in Clojure. In this essay, I’ll guide you through doing just that: we’ll
simulate NAND gates, and use about 14 thousand of them to build 256 bytes
of RAM.
Notice that the wires carry a charge, but we choose to interpret meaning in
the charge. “high charge” means 1, and “low charge” means 0. Nothing
changes in the machine, this is just something we decided as humans (1).
On the le you see a circuit diagram. You can read it as input wires a and
b carrying charges into the NAND gate. The NAND gate has a wire c ,
carrying the output charge. For all the circuit diagrams we’ll draw, you can
read them as electricity “ owing” from le to right, or top to bottom.
On the right is a “truth” table for a NAND gate. This is just a fancy name
for summarizing every state a NAND gate can be, based on the input wires.
Now, we can start even lower than a NAND gate , but this machine is
simple enough. It can’t be so hard to build something that turns o when
two inputs are turned on. You don’t have to take my word for it though,
you can search up “building a NAND gate with transistors”, and come back
when you’re convinced.
Time to code! 🙂
0: State
First things rst, we need some way to represent the state of our circuit.
We know that our RAM will be built completely from NAND gates, so let’s
take inspiration from one:
If we look at this example we can see that:
1. We have wires.
2. Wires have charges.
3. We hook wires together with NAND Gates
We can use keywords to represent our wires. We can also keep a map that
tells us the charges of our wires. Finally, we can keep a list of NAND gates,
which tell us how these wires connect.
Fine enough way to represent our circuit for now! Let’s create a few
functions that can help us manage this representation:
; update state v0
; ---------------
These are all the basic tools we need to “connect” a NAND gate into our
circuit. Let’s try them out in the REPL:
Nice! We can now “wire” up a circuit. Let’s run some electricity through it.
1: Trigger
To gure out how to simulate electricity into our circuit, let’s remember
our diagram again:
One way we can model this is to imagine that electricity is like water: It
“ ows” from sources into wires, and “triggers” all the devices that are
connected to those wires.
With a model like that, here’s what would happen if a charge was
“triggered” on a :
Now, this is a very naive view of how electricity works (2), but it’s good
enough for us to model RAM!
(defn nand-output [a b]
(if (= a b 1) 0 1))
(nand-output 0 0)
; => 1
(nand-output 1 1)
; => 0
Our nand-output function takes two input charges, and produces the
output charge that a NAND gate would produce.
Next, we need a function to nd all the NAND gates that are connected to a
speci c wire:
This searches all of our NAND gates in our circuit, and nds the ones which
are connected to a speci c wire.
(declare trigger-nand-gate)
(defn trigger
([state wire new-v]
(let [old-charge (charge state wire)
state' (set-charge state wire new-v)
new-charge (charge state' wire)]
(if (= old-charge new-charge)
state'
(reduce (fn [acc-state out] (trigger-nand-gate acc-state out))
state'
(dependent-nand-gates state' wire))))))
(defn trigger-nand-gate
[state {:keys [ins out]}]
(let [new-charge (apply nand-output (charges state ins))]
(trigger state out new-charge)))
This calculates the new charge of a NAND gate, and triggers the output
wire with that charge.
2: Simulate NAND
We have what we need to simulate a simple charge owing through a
NAND gate. Let’s write a test for that:
(deftest test-nand-gate
(let [s1 (-> empty-state
(wire-nand-gate :a :b :o)
(trigger-many [:a :b] [1 0]))
s2 (-> s1
(trigger :b 1))]
(testing "just a is on"
(is (= '(1 0 1) (charges s1 [:a :b :o]))))
(testing "both a and b are on"
(is (= '(1 1 0) (charges s2 [:a :b :o]))))))
3: Simulate NOT
What would happen, if we took a NAND gate, and fed the same wire in
both inputs?
Well, the output would end up being the opposite of its input. When a is
zero, c is 1, when a is 1, c is 0. Boom, that happens to be a NOT gate.
Here’s how that looks:
(defn wire-not-gate
([state a o]
(wire-nand-gate state a a o)))
It works! Onwards.
4: Simulate AND
What if we plugged the output of one NAND as the input of a NOT gate?
(defn wire
([n]
(let [i (uniq-n n)]
(if (> i 1) (kw n "#" i) n))))
Now if we create a wire with a name that already exists, it’ll add a nice
little “#2” beside it.
(deftest test-and-gate
(let [s1 (-> empty-state
(wire-and-gate :a :b :o)
(trigger-many [:a :b] [1 0]))
s2 (-> s1
(trigger :b 1))]
(testing "just a is on"
(is (= '(1 0 0) (charges s1 [:a :b :o]))))
(testing "a and b on"
(is (= '(1 1 1) (charges s2 [:a :b :o]))))))
5: Simulate Bits
Now comes one of the hardest and most important circuits we’ll need to
understand. Let’s start by describing our goal:
Notice the interesting thing here. When the s wire is “1”, the value of i
is transferred to o . When it is “0”, the value of o is no longer a ected by i.
o 's charge is whatever it was before.
If we can make something like this, that would mean that the charge on
“o” is stored. Since it can be either 1 or 0, we can in e ect “store” 1 bit of
data.
The trick with this circuit is the way o and c are connected together.
This intertwining thing is called a “latch”, because once a charge gets set
in a certain way, these gates will nd an equilibrium that causes o to be
stored. Pretty cool!
This circuit is pretty complicated and a bit hard to understand (3), but
we’ve got the power of simulation at our ngertips! Let’s try building it
and test it out:
(defn wire-memory-bit
"To understand the variables in this circuit,
follow along with the diagram in the tutorial"
([state s i o]
(let [a (wire :a)
b (wire :b)
c (wire :c)]
(-> state
(wire-nand-gate i s a)
(wire-nand-gate a s b)
(wire-nand-gate a c o)
(wire-nand-gate b o c)))))
(deftest test-memory-bit
(let [s1 (-> empty-state
(wire-memory-bit :s :i :o)
(trigger-many [:i :s] [1 0]))
s2 (-> s1
(trigger :s 1))
s3 (-> s2
(trigger-many [:s :i] [0 0]))]
(testing "turning i on does not affect the rest"
(is (= '(0 1 0) (charges s1 [:s :i :o]))))
(testing "enabling set transfers i to o"
(is (= '(1 1 1)
(charges s2 [:s :i :o]))))
(testing "disabling set, removes further effects on o"
(is (= '(0 0 1)
(charges s3 [:s :i :o]))))))
Oh ma gad…it works!
6: Simulate Bytes
Now that we have a bit, we can take one s wire and tie 8 memory bits
together with it. That would let us set 8 bits together, which means we
can “store” 8 bits of data…which gives us a byte! (5). Here’s how that would
look:
Note that in our diagram now “two wires together” is short-hand for
writing 8 wires.
Easy peasy!
To test this out though, we’re going to need a way to “create” a bunch of
names for wires. Let’s write a few helper functions that make this easy:
(defn names [n r]
(mapv (fn [i] (kw n "-" i)) (range r)))
(def wires (comp (partial mapv wire) names))
(wires :i 8)
; => [:i-0 :i-1 :i-2 :i-3 :i-4 :i-5 :i-6 :i-7]
(deftest test-byte
(let [ii (wires :i 8)
os (wires :o 8)
s1 (-> empty-state
(wire-byte :s ii os)
(trigger-many ii [1 1 1 0 0 0 0 0])
(trigger :s 0))
s2 (-> s1
(trigger :s 1))
s3 (-> s2
(trigger :s 0)
(trigger-many ii [0 0 0 0 0 0 0 1]))]
(testing "disabling set, removes further effects on o"
(is (= '(1 1 1 0 0 0 0 0)
(charges s1 ii)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s1 os))))
(testing "set s, so os become 1 1 1"
(is (= '(1 1 1 0 0 0 0 0)
(charges s2 ii)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s2 os))))
(testing "freeze by disabling s. see that further changes to i do noth
(is (= '(0 0 0 0 0 0 0 1)
(charges s3 ii)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s2 os))))))
Phoof…does it work?
Yes!
7: Simulate Enabler
Now, imagine we had two bytes, connected like this:
Notice how the output wires are shared between B1 and B2 . If B1 had a
charge of “11110000”, and B2 had a charge of “0001111”, what would
happen to the output wires? It would carry a charge of “1111111”! Say we
wanted to make sure only one of the bytes sent their output charge into
output wires . How could we do that?
We’ll need a new machine. Let’s consider what would happen if we took a
bunch of AND gates, and connected them together like this:
Now, if the “e” wire is “on”, the output wires are charged with whatever the
input wires are. Buut, if the “e” wire is “o ”, the output zeroes out. This
machine is called the “enabler”. If we put this together right, we could
control what charge gets sent to output wires !
(defn wire-enabler
[state e ins outs]
(reduce
(fn [acc-state [in out]]
(wire-and-gate acc-state e in out))
state
(map vector ins outs)))
(deftest test-enabler
(let [ii (wires :i 8)
os (wires :o 8)
s1 (-> empty-state
(wire-enabler :e ii os)
(trigger-many ii [1 1 1 0 0 0 0 0])
(trigger :e 0))
s2 (trigger s1 :e 1)
s3 (trigger s2 :e 0)]
(testing "is should be set, but os should be 0"
(is (= '(1 1 1 0 0 0 0 0)
(charges s1 ii)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s1 os))))
(testing "os should pass if enabled"
(is (= '(1 1 1 0 0 0 0 0)
(charges s2 ii)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s2 os))))
(testing "os should revert if disabled"
(is (= '(1 1 1 0 0 0 0 0)
(charges s3 ii)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s3 os))))))
🔥
8: Simulate Register
Here’s how we could “ x” our problem with B1 and B2 :
To set this up, all we need to do is to tie together a byte and an enabler:
(deftest test-register
(let [ii (wires :i 8)
bs (wires :b 8)
os (wires :o 8)
s1 (-> empty-state
(wire-register :s :e ii bs os)
(trigger-many ii [1 1 1 0 0 0 0 0])
(trigger :s 0)
(trigger :e 0))
s2 (trigger s1 :s 1)
s3 (trigger s2 :e 1)
s4 (-> s3
(trigger :s 0)
(trigger-many ii [0 0 0 0 0 0 0 1]))
s5 (trigger s4 :e 0)]
(testing "is should be set, but bs and os should be 0, b/c s & e are
(is (= '(1 1 1 0 0 0 0 0)
(charges s1 ii)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s1 bs)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s1 os))))
(testing "is & bs should be set, as s is on. but os should be 0, b/c e
(is (= '(1 1 1 0 0 0 0 0)
(charges s2 ii)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s2 bs)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s2 os))))
(testing "is & bs should be set, as s is on. but os should be 0, b/c e
(is (= '(1 1 1 0 0 0 0 0)
(charges s3 ii)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s3 bs)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s3 os))))
(testing "is should be new v, but bs and os should be the old value"
(is (= '(0 0 0 0 0 0 0 1)
(charges s4 ii)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s4 bs)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s4 os))))
(testing "os should 0 out again"
(is (= '(0 0 0 0 0 0 0 1)
(charges s5 ii)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s5 bs)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s5 os))))))
Very cool!
9: Simulate Bus
Okay, let’s continue our experiment, to an astounding result: what if we
connected the inputs and outputs of a bunch of registers to the same wires?
Now, remember that s allows us to decide what gets “stored” into a
register, and “e” lets us “pass” the charge of a register’s byte through to the
output.
What would happen in the following scenario. Say R1’s byte contains “111”,
all s and e wires are 0.
1. “Charge R1 ’s e to 1”.
1. Now R1 would enable, and the bus wires would carry the
same charge as R1
2. “Charge R3 ’s s to 1, then 0”.
1. This would set the value of R3 , to the current charge
owing in bus . This happens to be the output of R1 !
3. “Set R1 's e to 0” Now the current in bus would disappear again
The result? The byte in R1 would have been “copied” to R3. We’ve just
created a bus .
This is only a single line, but it’s pretty important to get right. It’s what lets
us “copy” registers a er all. Let’s see if it works:
(deftest test-wire-bus
(let [bw (wires :bw 8)
r1-bits (wires :r1 8)
r2-bits (wires :r2 8)
r3-bits (wires :r2 8)
s1 (-> empty-state
(wire-bus bw :s1 :e1 r1-bits)
(wire-bus bw :s2 :e2 r2-bits)
(wire-bus bw :s3 :e3 r3-bits)
(trigger-many bw [1 1 1 0 0 0 0 0])
(trigger :s1 0)
(trigger :e1 0))
s2 (-> s1
(trigger :s1 1)
(trigger :s1 0)
(trigger-many bw [0 0 0 0 0 0 0 0]))
s3 (-> s2
(trigger :e1 1)
(trigger :s3 1)
(trigger :s3 0)
(trigger :e1 0))]
(testing "only bus should have charge"
(is (= '(1 1 1 0 0 0 0 0)
(charges s1 bw)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s1 r1-bits)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s1 r2-bits)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s1 r3-bits))))
(testing "r1 should have the charge"
(is (= '(0 0 0 0 0 0 0 0)
(charges s2 bw)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s2 r1-bits)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s2 r2-bits)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s2 r3-bits))))
(testing "move r1 to r3"
(is (= '(0 0 0 0 0 0 0 0)
(charges s3 bw)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s3 r1-bits)))
(is (= '(0 0 0 0 0 0 0 0)
(charges s2 r2-bits)))
(is (= '(1 1 1 0 0 0 0 0)
(charges s3 r3-bits))))))
Remember that the bus wires can “receive” a charge now from the
outputs of R1 , R2 , or R3 . Here’s how our set-charge looked:
Time to to evolve our model. One way to think about it, is that bus wires
now have multiple “sources” that can provide a charge. Let’s update our
charge functions to keep track of this “source”
(defn set-charge
([source state w v]
(assoc-in state [:charge-map w source] v)))
(defn trigger
([state wire new-v] (trigger :repl state wire new-v))
([source state wire new-v]
(let [old-charge (charge state wire)
state' (set-charge state source wire new-v)
new-charge (charge state' wire)]
(if (= old-charge new-charge)
state'
(reduce (fn [acc-state out] (trigger-nand-gate acc-state out))
state'
(dependent-nand-gates state' wire))))))
(defn trigger-nand-gate
[state {:keys [ins out]}]
(let [new-charge (apply nand-output (charges state ins))]
(trigger (apply kw (conj ins out)) state out new-charge)))
Now each NAND gate triggers with a speci c name as a source. This would
make it so R1 's output trigger wouldn’t interfere with R2 output trigger!
Let’s test out the bus again:
We’re back.
Let’s call this the AND-N gate. It can take N number of inputs, and
produces one output. We can wire this up using some nice recursion:
(deftest test-and-n
(let [ii [:a :b :c :d :e]
s1 (-> empty-state
(wire-and-n ii :o)
(trigger-many ii [1 1 1 1 0]))
s2 (trigger-many s1 ii [1 1 1 1 1])]
(testing "if only some are charged, o is off"
(is (= '(1 1 1 1 0)
(charges s1 ii)))
(is (= 0 (charge s1 :o))))
(testing "if all are on, we are on"
(is (= '(1 1 1 1 1)
(charges s2 ii)))
(is (= 1 (charge s2 :o))))))
This looks like a lot, but in essence it’s pretty simple. Each input is wired
into a NOT gate. Now let’s take an example: what would happen If we wire
the NOT outputs of a & b to an AND gate?
If that AND output turned “1”, it would mean that both a and b had to be
0! This would mean the output wire of that AND gate, when “1”, represents
the selection (a0 b0). We can keep wiring AND gates like this, to represent
all di erent selections.
With this we can create a quick function that produces our “wire”
selection mapping:
(decoder-mapping 2)
; => ((0 0) (0 1) (1 0) (1 1))
(defn wire-decoder
[state ins outs]
(let [ins-nots (mapv #(wire (kw % :-not)) ins)
state' (reduce
(fn [acc-state [in out]]
(wire-not-gate acc-state in out))
state
(map vector ins ins-nots))
state'' (reduce
(fn [acc-state [sel out]]
(let [and-ins (map-indexed
(fn [i sign]
(if (= sign 0)
(nth ins-nots i)
(nth ins i)))
sel)]
(wire-and-n acc-state and-ins out)))
state'
(map vector (wire-mapping (count ins)) outs))]
state''))
(deftest test-decoder
(let [ii (wires :i 4)
os (wires :o 16)
sels (wire-mapping (count ii))
sel (nth sels 5)
o (nth os 5)
s1 (-> empty-state
(wire-decoder ii os)
(trigger-many ii sel))]
(testing "only 1 output is on"
(is (= 1 (charge s1 o)))
(is (every? zero? (charges s1 (remove #{o} os)))))))
😦
13: Simulate Lookup
With all of this, we can wire together the rst part of RAM. We need a way
to “look up” a byte.
What if we took one byte — let’s call it mar — and wired the rst 4
outputs to one decoder, and the last 4 outputs to another decoder?
Well, this would produce 256 output wires. For each value in the mar
byte, two output wires would be on: one from the rst decoder, and the
other from the second decoder.
We can think of this then, like a 16x16 grid. Each “byte” in mar represents
a unique “intersection” in the grid. Now, in the future we can tie some
registers to those grid wires, and when an “intersection” is on, we can
make that register active! Pretty cool.
(defn wire-mar
[state s is os first-4-outs last-4-outs]
(-> state
(wire-byte s is os)
(wire-decoder (take 4 os) first-4-outs)
(wire-decoder (drop 4 os) last-4-outs)))
Will it work?
(deftest test-mar
(let [ii (wires :i 8)
os (wires :o 8)
first-4-decoders (wires :fd 16)
last-4-decoders (wires :ld 16)
s1 (-> empty-state
(wire-mar :s ii os first-4-decoders last-4-decoders)
(trigger-many ii [0 0 0 0 0 0 0 0])
(trigger :s 0))
sel (nth (wire-mapping 4) 5)
s2 (trigger-many s1 ii (concat sel sel))
s3 (-> s2
(trigger :s 1)
(trigger :s 0))
test-idx (fn [state idx]
(let [fd (nth first-4-decoders idx)
ld (nth last-4-decoders idx)]
(is (= 1 (charge state fd)))
(is (every? zero? (charges state (remove #{fd} first-
(is (= 1 (charge state ld)))
(is (every? zero? (charges state (remove #{ld} last-
(testing
"by default only one wire is on, and it's the correct mapping"
(test-idx s1 0))
(testing
"even if ii changes, sel doesn't change b/c s is 0"
(test-idx s2 0))
(testing
"once s triggers, the sel does change"
(test-idx s3 5))))
14: Simulate IO
Time to complete the picture:
We’re going to add an io bus at the bottom, a set wire an enable wire.
For each “intersection” from our decoders, what would happen if we did
this?
The top decoder wire and left decoder wire are fed into an AND gate
that outputs x . x will only be “on” when both decoder wires are “on”.
That means x would represent whether our intersection is active!
x and io-s are fed into an AND gate, which produces s. s will only
turn on, when both io-s is charged and the intersection is active.
x and io-e are fed an AND gate, producing e . This will only turn on,
when both io-e is charged, and the intersection is active.
If we follow that logic, it means that this register can be enabled and set,
only when the intersection is “on”! If hooked this up to each intersection…
all of a sudden we have 256 bytes of memory!
(defn wire-io
[state io-s io-e ios decoder-o-1 decoder-o-2 register-bits]
(let [x (wire (kw decoder-o-1 decoder-o-2 :x))
s (wire (kw decoder-o-1 decoder-o-2 :s))
e (wire (kw decoder-o-1 decoder-o-2 :e))]
(-> state
(wire-and-gate decoder-o-1 decoder-o-2 x)
(wire-and-gate x io-s s)
(wire-and-gate x io-e e)
(wire-bus ios s e register-bits))))
Does it work?
(deftest test-io
(let [ios (wires :io 8)
rs (wires :r 8)
s1 (-> empty-state
(wire-io :s :e ios :w1 :w2 rs)
(trigger-many ios [0 0 0 0 0 0 0 0])
(trigger-many [:s :e :w1 :w2] [0 0 0 0])
(trigger-many ios [1 1 1 0 0 0 0 0]))
s2 (trigger s1 :s 1)
s3 (trigger-many s2 [:w1 :w2] [1 1])
s4 (-> s3
(trigger :s 0)
(trigger-many ios [0 0 0 0 0 0 0 0]))
s5 (trigger s4 :e 1)]
(testing "io set, but reg not affected"
(is (= '(1 1 1 0 0 0 0 0) (charges s1 ios)))
(is (= '(0 0 0 0 0 0 0 0) (charges s1 rs))))
(testing "io set enable doesn't change, because intersection is not on
(is (= '(1 1 1 0 0 0 0 0) (charges s2 ios)))
(is (= '(0 0 0 0 0 0 0 0) (charges s2 rs))))
(testing "once intersection is on, charge transfers"
(is (= '(1 1 1 0 0 0 0 0) (charges s3 ios)))
(is (= '(1 1 1 0 0 0 0 0) (charges s3 rs))))
(testing "once s turns off again, changes to io don't make a differenc
(is (= '(0 0 0 0 0 0 0 0) (charges s4 ios)))
(is (= '(1 1 1 0 0 0 0 0) (charges s4 rs))))
(testing "if we turn on e, the r charge transfers to the io"
(is (= '(1 1 1 0 0 0 0 0) (charges s5 ios)))
(is (= '(1 1 1 0 0 0 0 0) (charges s5 rs))))))
Great!
That’s a lot. Let’s try triggering something. How long does it take?
Every time a wire is triggered, we go through all 14056 NAND gates! That’s
a lot of iteration for nding what is probably a few NAND gates.
Here, we make accessing dependent NAND gates fast. Now it’s a matter of a
dictionary lookup. If we try it out, it’ll be muuch faster!
1. We wire up mar switches to the mar byte, and add a switch to the
set wire
1. Depending on which switches we turn on, we can set a speci c
byte in mar , and access a speci c “register” in our grid!
2. We wire up io lights
With a machine like that, we can now use our RAM! Here’s how.
1. Use the MAR switches , to select the exact intersection we want, and
toggle the mar set switch
2. Toggle the io enable switch . This will send the active register’s data
into io
3. Look at the io lights : The combination of lights that are on
represent the value of the register!
4. When done, toggle the io enable switch o .
Next, here’s a quick helper function to “set” data into our mar byte:
Here’s the function that follows our recipe to “access a speci c register”
Here’s the function that follows our recipe to “set data to a speci c
register”
(defn ram-repl []
(println
(str " 🔥 Ram Simulation: Type a command. Here's what you can do: \n"
" (read [1 0 1 0 1 0 1 0]) \n"
" (write \[1 0 1 0 1 0 1 0\] [1 1 1 1 1 1 1 1]) \n"
" (exit)"))
(let [mar-is (wires :mar-i 8)
ios (wires :mar-io 8)
initial-state (initialize-ram :mar-s mar-is :io-s :io-e ios)]
(loop [state initial-state]
(let [input (read-string (read-line))
cmd (first input)
args (rest input)]
(condp = cmd
'read
(recur (handle-read state :mar-s mar-is :io-e ios (first args))
'write
(recur (handle-write state :ms mar-is :io-s ios (first args) (se
'exit
(println "> Goodbye!"))))))
GIF
Fin
Wow. we did it. 14056 NAND gates. 256 bytes of RAM. I hope you had a
blast 🙂 — If you want to see how the rest of the computer is made, I
de nitely suggest checking out Scott’s book. He also has a great site with
more example projects. To see the code all together, here’s the repo.
Thanks to Daniel Woelfel, Julien Odent, Sean Grove, Paul Vorobyev, Alexander
Kotliarskyi, Alex Reichert, Poornamidam for reviewing dra s of this essay.
(1) I say this pretty lightly, but treating the charge on circuits as code was a
pretty phenomenal innovation. I could only imagine what it might have
been like, when Shannon showed a full-adder.
(2) To really model circuits, we’d need to learn about Maxwell’s equations.
Just ordered a textbook for this, so maybe next time 🙂.
(4) That stack exchange question helped me a lot. If you’re even more
curious, start with this youtube video, and follow along until the
gentleman gets to the “D” latch. He uses NOR gates, but the essence is the
same.
(5) There were a lot of di erent kind of architectures that were tried.