Carry-lookahead adder in Python
Computes carries from generate and propagate signals in logarithmic depth.
- File
carry_lookahead_adder.py, 149 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/carry-lookahead-adder/. The authors are listed in prov/ZGRFB8.jsonl.
"""A gate-level model of the hierarchical carry-lookahead adder.
A wire is a pair (bit, arrival time). Every AND, OR and XOR gate has delay
1, whatever its fan-in. The lookahead units have r children each, so the
largest fan-in is r + 1. The function returns the sum bits, the carry out,
the gate counts and the depth of the circuit, so that tests can check both
the values and the cost claims of the page.
"""
from dataclasses import dataclass, field
Wire = tuple[int, int]
@dataclass
class Counts:
"""Gates used by the circuit, and the largest fan-in seen."""
and_gates: int = 0
or_gates: int = 0
xor_gates: int = 0
max_fan_in: int = 0
levels: int = 0
@dataclass
class Node:
"""A node of the lookahead tree: its (G, P) pair and its children."""
g: Wire
p: Wire | None
children: list["Node"] = field(default_factory=list)
leaf: int = -1
carry_in: Wire | None = None
def _and(ws: list[Wire], k: Counts) -> Wire:
if len(ws) == 1:
return ws[0]
k.and_gates += 1
k.max_fan_in = max(k.max_fan_in, len(ws))
return (int(all(w[0] for w in ws)), max(w[1] for w in ws) + 1)
def _or(ws: list[Wire], k: Counts) -> Wire:
if len(ws) == 1:
return ws[0]
k.or_gates += 1
k.max_fan_in = max(k.max_fan_in, len(ws))
return (int(any(w[0] for w in ws)), max(w[1] for w in ws) + 1)
def _xor(x: Wire, y: Wire, k: Counts) -> Wire:
k.xor_gates += 1
return (x[0] ^ y[0], max(x[1], y[1]) + 1)
def _lookahead(ch: list[Node], c: Wire | None, k: Counts) -> Wire:
"""g[m-1] or p[m-1] g[m-2] or ... or p[m-1] ... p[0] c, for m = len(ch)."""
m = len(ch)
terms = [ch[m - 1].g]
for j in range(m - 2, -1, -1):
terms.append(_and([ch[t].p for t in range(m - 1, j, -1)] + [ch[j].g], k))
if c is not None:
terms.append(_and([ch[t].p for t in range(m - 1, -1, -1)] + [c], k))
return _or(terms, k)
def cla_add(a: list[int], b: list[int], c0: int = 0, r: int = 4):
"""Add two n-bit numbers given low bit first.
Returns (sum bits, carry out, counts, depth).
"""
n = len(a)
assert n == len(b) and n >= 1 and r >= 2
k = Counts()
ins_a = [(x, 0) for x in a]
ins_b = [(y, 0) for y in b]
cin: Wire = (c0, 0)
level = []
for i in range(n):
g = _and([ins_a[i], ins_b[i]], k) # @line L1
p = _xor(ins_a[i], ins_b[i], k) # @line L1
level.append(Node(g, p, leaf=i)) # @line L2
h = 0 # @line L2
while True: # @line L3
h += 1 # @line L4
groups = [level[j:j + r] for j in range(0, len(level), r)] # @line L4
root = len(groups) == 1
nxt = []
for ch in groups: # @line L5
if root:
nxt.append(Node((0, 0), None, ch))
else:
big_g = _lookahead(ch, None, k) # @line L5
big_p = _and([w.p for w in ch], k) # @line L5
nxt.append(Node(big_g, big_p, ch))
level = nxt
if root: # @line L6
break
k.levels = h
top = level[0]
top.carry_in = cin # @line L7
stack = [top]
carries: list[Wire] = [cin] * n
while stack: # @line L8
node = stack.pop()
if node.leaf >= 0:
carries[node.leaf] = node.carry_in
continue
ch = node.children
ch[0].carry_in = node.carry_in # @line L9
for t in range(1, len(ch)):
ch[t].carry_in = _lookahead(ch[:t], node.carry_in, k) # @line L9
stack.extend(ch)
cout = _lookahead(top.children, cin, k) # @line L10
leaves = _leaves(top)
s = [_xor(leaves[i].p, carries[i], k) for i in range(n)] # @line L11
depth = max([w[1] for w in s] + [cout[1]])
return [w[0] for w in s], cout[0], k, depth # @line L12
def _leaves(top: Node) -> list[Node]:
out, stack = [], [top]
while stack:
node = stack.pop()
if node.leaf >= 0:
out.append(node)
else:
stack.extend(node.children)
out.sort(key=lambda x: x.leaf)
return out
def to_bits(x: int, n: int) -> list[int]:
return [(x >> i) & 1 for i in range(n)]
def from_bits(bits: list[int]) -> int:
return sum(bit << i for i, bit in enumerate(bits))
def add(x: int, y: int, n: int, c0: int = 0, r: int = 4) -> int:
"""x + y + c0 for 0 <= x, y < 2^n, through the circuit."""
s, cout, _, _ = cla_add(to_bits(x, n), to_bits(y, n), c0, r)
return from_bits(s) + (cout << n)
test_carry_lookahead_adder.py 85 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/carry-lookahead-adder/. The authors are listed in prov/ZGRFB8.jsonl.
import random
from carry_lookahead_adder import add, cla_add, from_bits, to_bits
def _levels(n: int, r: int) -> int:
h, size = 0, n
while True:
h += 1
size = -(-size // r)
if size == 1:
return h
def test_example() -> None:
s, cout, _, depth = cla_add(to_bits(0xA7, 8), to_bits(0x59, 8))
assert s == [0] * 8 and cout == 1
assert from_bits(s) + (cout << 8) == 256
assert depth == 7
def test_exhaustive_small() -> None:
for r in (2, 3, 4):
for n in range(1, 6):
for x in range(1 << n):
for y in range(1 << n):
for c0 in (0, 1):
assert add(x, y, n, c0, r) == x + y + c0
def test_edges() -> None:
for r in (2, 4, 5):
for n in (1, 2, 4, 15, 16, 17, 64, 100):
top = (1 << n) - 1
assert add(0, 0, n, 0, r) == 0
assert add(top, 0, n, 1, r) == 1 << n
assert add(top, top, n, 1, r) == 2 * top + 1
assert add(top, 1, n, 0, r) == 1 << n
assert add(0x5555555555555555555555555 & top, 0xAAAAAAAAAAAAAAAAAAAAAAAAA & top, n, 1, r) == top + 1
def test_random() -> None:
rng = random.Random(20261107)
for _ in range(600):
n = rng.randint(1, 96)
r = rng.choice((2, 3, 4, 8))
x, y = rng.getrandbits(n), rng.getrandbits(n)
c0 = rng.randint(0, 1)
assert add(x, y, n, c0, r) == x + y + c0
def test_depth_and_fan_in() -> None:
for r in (2, 3, 4, 8):
for n in list(range(1, 40)) + [63, 64, 65, 127, 128, 129, 256, 257]:
x = (1 << n) - 1
_, _, k, depth = cla_add(to_bits(x, n), to_bits(0, n), 1, r)
h = _levels(n, r)
assert k.levels == h
assert depth <= 4 * h
assert k.max_fan_in <= r + 1
def test_exact_depth_at_powers() -> None:
for r in (2, 4):
n = r
while n <= 1024:
_, _, k, depth = cla_add([1] * n, [0] * n, 1, r)
expect = 4 * k.levels - (1 if r == 2 and k.levels >= 2 else 0)
assert depth == expect
n *= r
def test_linear_size() -> None:
for n in (2, 4, 8, 16, 64, 256, 1024):
_, _, k, _ = cla_add([0] * n, [0] * n, 0, 2)
assert k.and_gates + k.or_gates + k.xor_gates == 8 * n - 5
for n in (4, 16, 64, 256, 1024):
_, _, k, _ = cla_add([0] * n, [0] * n, 0, 4)
assert 3 * (k.and_gates + k.or_gates + k.xor_gates) == 23 * n - 14
for n in range(1, 130):
_, _, k, _ = cla_add([0] * n, [0] * n, 0, 4)
assert k.and_gates + k.or_gates + k.xor_gates <= 8 * n
What the next level needs
- the tests did not run in this build