---
title: Combinational Logic and HCL
module: Digital Logic
moduleNumber: 3
lessonNumber: 2
order: 302
summary: >
  A combinational circuit is a pure Boolean function of its current inputs — no
  memory, no clock. We draw the line between combinational and sequential logic,
  do the gate-delay accounting that finds a circuit's critical path and bounds
  the clock, meet don't-cares, then introduce CS:APP's Hardware Control Language:
  bit-level operators, word-level signals, equality nets, and the case expression
  that compiles to a multiplexer tree.
topics: [Digital Logic]
sources:
  - book: Bryant & O'Hallaron
    ref: "CS:APP — §4.2 Logic Design and the Hardware Control Language"
---

The [previous lesson](/computer-architecture/digital-logic/transistors-gates-and-boolean-functions)
built gates and wired a few together by hand. That does not scale: a real datapath
has thousands of gates, and drawing each one is hopeless. Engineers describe
hardware in **text** instead, with a hardware description language, and a tool
turns the text into gates. CS:APP uses a small teaching language called **HCL**
(Hardware Control Language) for exactly this, and the Y86-64 processor we build
later is written in it. This lesson covers what kind of logic HCL describes
(**combinational** logic) and the handful of constructs the language provides.

## Combinational versus sequential

Every digital circuit falls into one of two camps, and the distinction is about
**memory**.

> **Definition (Combinational circuit).** A network of gates whose outputs are a
> pure function of its **current** inputs. It has no memory: hold the inputs fixed
> and, after a brief propagation delay, the outputs settle to one fixed value
> determined entirely by those inputs. The same inputs always give the same
> outputs.

A **sequential** circuit, by contrast, has **state**: its outputs depend on the
history of inputs, not just the present ones. The memory elements of the next two
lessons (latches, flip-flops, registers) are sequential; everything in this lesson
and the [next](/computer-architecture/digital-logic/multiplexers-decoders-and-the-alu)
is combinational. Two rules keep a network combinational:

- Every input to each gate is either a primary input or the output of another
  gate. No wire dangles.
- There are **no cycles**: no path leads from a gate's output back to its own
  input. Feedback is what creates memory, so forbidding it guarantees
  there is none.

The mental model is a cloud of gates: signals enter on the left, ripple through,
and emerge on the right, with the only delay being the time for the slowest path
of gates to settle.

$$
% caption: A combinational block is a Boolean function of its current inputs. Input
% caption: signals enter a cloud of acyclic gate logic and, after propagation,
% caption: stable outputs emerge. No feedback, no memory.
\begin{tikzpicture}[font=\footnotesize,>=stealth,
  io/.style={draw, minimum width=10mm, minimum height=6mm, inner sep=1pt}]
  \definecolor{acc}{HTML}{2348F2}
  % inputs
  \foreach \y/\lab in {1.6/a, 0.8/b, 0.0/c}
    \node[io] (in\lab) at (0,\y) {$\lab$};
  % combinational cloud as a rounded block
  \node[draw, fill=acc!8, minimum width=34mm,
        minimum height=26mm, align=center] (cl) at (3.4,0.8)
        {combinational\\logic\\(acyclic gates)};
  % outputs
  \node[io] (outx) at (6.8,1.2) {$x$};
  \node[io] (outy) at (6.8,0.4) {$y$};
  % wires in
  \draw (ina.east) -- (ina.east -| cl.west);
  \draw (inb.east) -- (inb.east -| cl.west);
  \draw (inc.east) -- (inc.east -| cl.west);
  % wires out
  \draw (outx.west -| cl.east) -- (outx.west);
  \draw (outy.west -| cl.east) -- (outy.west);
  \node[text=acc] at (3.4,-1.2) {no path returns to its own input};
\end{tikzpicture}
$$

## Gate delays and the critical path

"After a brief propagation delay" hides the number that decides how fast a
processor can run. Each gate contributes the $t_{pd}$ of the
[previous lesson](/computer-architecture/digital-logic/transistors-gates-and-boolean-functions),
and delays **accumulate along paths**: a gate's output cannot begin to settle
until its latest-arriving input has, so the arrival time at any wire is the
maximum over the paths feeding it. The output of the whole block settles when
its slowest path does.

The accounting is mechanical. Take a small network with three gates: an AND
and an OR at 25 ps each, and an XOR at 75 ps (internally it is three gate levels:
inverters, ANDs, OR), wired so that one input must traverse all three.

$$
% caption: Gate-delay accounting. Inputs are valid at t = 0; each gate adds its
% caption: delay to its latest-arriving input. The highlighted path a - g1 - g2 - g3
% caption: accumulates 25 + 25 + 75 = 125 ps and is the critical path: no output of
% caption: this block can be trusted before 125 ps.
\begin{tikzpicture}[font=\footnotesize,>=stealth,
  g/.style={draw, minimum width=13mm, minimum height=8mm, inner sep=1pt}]
  \definecolor{acc}{HTML}{2348F2}
  \node[g, fill=acc!8] (g1) at (0,1.95) {AND};
  \node[g, fill=acc!8] (g2) at (2.6,1.4) {OR};
  \node[g, fill=acc!8] (g3) at (5.4,0.4) {XOR};
  \node[anchor=north] at (g1.south) {25 ps};
  \node[anchor=north] at (g2.south) {25 ps};
  \node[anchor=north] at (g3.south) {75 ps};
  % inputs
  \draw[acc, thick] (-2.0,2.13) node[anchor=east, text=black] {$a$} -- (-0.65,2.13);
  \draw (-2.0,1.77) node[anchor=east] {$b$} -- (-0.65,1.77);
  \draw (-2.0,0.9) node[anchor=east] {$c$} -- (1.55,0.9) -- (1.55,1.22) -- (1.95,1.22);
  \draw (-2.0,-0.6) node[anchor=east] {$d$} -- (4.3,-0.6) -- (4.3,0.22) -- (4.75,0.22);
  % path g1 -> g2 -> g3 -> f (critical, highlighted)
  \draw[acc, thick] (g1.east) -- (1.55,1.95) -- (1.55,1.58) -- (1.95,1.58);
  \draw[acc, thick] (g2.east) -- (4.05,1.4) -- (4.05,0.58) -- (4.75,0.58);
  \draw[acc, thick] (g3.east) -- ++(1.1,0) node[anchor=west, text=acc] {$f$};
  % arrival-time labels, clear of wires
  \node[anchor=south] at (-1.2,2.2) {$t{=}0$};
  \node[anchor=south, text=acc] at (1.2,2.0) {$t{=}25$};
  \node[anchor=south, text=acc] at (3.7,1.46) {$t{=}50$};
  \node[anchor=south, text=acc] at (6.55,0.46) {$t{=}125$};
\end{tikzpicture}
$$

Input $d$ reaches the XOR at $t = 0$ but does not help: the XOR's other input
arrives at $t = 50$, and only then does its own 75 ps start counting. The wire
$f$ settles at $t = 125$ ps, and the path that set the number — $a$ through all
three gates — is the **critical path**.

> **Definition (Critical path).** The slowest input-to-output path through a
> combinational network, measured as the sum of gate delays along it. The
> network's outputs are guaranteed stable only after the critical-path delay has
> elapsed since the last input change.

The critical path is what bounds the **clock**. In the synchronous designs of
[lesson 4](/computer-architecture/digital-logic/memory-elements-latches-flip-flops-and-clocking),
combinational blocks sit between storage elements, and the clock period must be
long enough for the slowest block to settle: a path forty gate levels deep at
25 ps per level needs a nanosecond, capping the clock at 1 GHz no matter how
fast every other block is. Making a processor faster is therefore mostly a matter
of shortening the worst path, not the average one: replace a deep circuit with a
shallow one (the carry-lookahead adder of the
[next lesson](/computer-architecture/digital-logic/multiplexers-decoders-and-the-alu)
is the classic case), or cut a long path in half with a register and spend two
cycles, which is the whole idea of pipelining, later in the course.

One more consequence of unequal path delays: on the way to settling, an output
may pass through values that are wrong. If two inputs of an OR gate swap roles,
one falling and the other rising a moment later, the output can dip low for the
skew between them, a transient called a **glitch** (or hazard).

$$
% caption: A glitch. Input x falls before input y rises, so their OR dips low for
% caption: the interval between the two edges before settling back to 1. The final
% caption: value is correct; the waveform on the way there is not.
\begin{tikzpicture}[font=\footnotesize,>=stealth]
  \definecolor{acc}{HTML}{2348F2}
  \node[anchor=east] at (-0.3,2.4) {$x$};
  \draw (0,2.6) -- (1.2,2.6) -- (1.2,2.2) -- (5.6,2.2);
  \node[anchor=east] at (-0.3,1.4) {$y$};
  \draw (0,1.2) -- (1.8,1.2) -- (1.8,1.6) -- (5.6,1.6);
  \node[anchor=east, text=acc] at (-0.3,0.4) {$x$ OR $y$};
  \draw[acc, thick] (0,0.6) -- (1.5,0.6) -- (1.5,0.2) -- (2.1,0.2)
        -- (2.1,0.6) -- (5.6,0.6);
  \draw[dashed, black] (1.2,2.2) -- (1.2,-0.15);
  \draw[dashed, black] (1.8,1.2) -- (1.8,-0.15);
  \node[anchor=north, text=acc] at (1.8,-0.25) {hazard: a brief false 0};
\end{tikzpicture}
$$

Combinational logic guarantees only the settled value, not the waveform on the
way there: outputs are meaningful **after settling**, and the clock enforces
that nothing samples earlier. That contract (wait out the worst case, then
sample) is the reason glitches are harmless in a properly clocked design.

### Working the arrival times as a table

The graphical accounting scales to a spreadsheet. Number the gates, and for each
one write down when its inputs are ready and add its own delay. The arrival time
at a gate's output is $\max(\text{input arrivals}) + t_{pd}$; the block's outputs
are ready at the max over all of them. Take a slightly bigger network: inputs
$a, b, c, d, e$ valid at $t = 0$, and five gates wired

$$
g_1 = a \wedge b,\quad
g_2 = c \vee d,\quad
g_3 = g_1 \oplus e,\quad
g_4 = g_2 \wedge g_3,\quad
f = g_4 \vee g_1,
$$

with delays AND/OR $= 20$ ps, XOR $= 60$ ps. Fill the table left to right, each row
depending only on rows above it:

| gate | inputs | input arrival (max) | $+\,t_{pd}$ | output ready |
| --- | --- | --- | --- | --- |
| $g_1$ | $a, b$ | $0$ | $20$ | $20$ |
| $g_2$ | $c, d$ | $0$ | $20$ | $20$ |
| $g_3$ | $g_1, e$ | $\max(20, 0) = 20$ | $60$ | $80$ |
| $g_4$ | $g_2, g_3$ | $\max(20, 80) = 80$ | $20$ | $100$ |
| $f$ | $g_4, g_1$ | $\max(100, 20) = 100$ | $20$ | $120$ |

The output $f$ settles at $120$ ps, and tracing the max backward names the
critical path: $f \leftarrow g_4 \leftarrow g_3 \leftarrow g_1 \leftarrow a$
(or $b$). The XOR $g_3$ dominates because its 60 ps sits on that chain; the
parallel branch through $g_2$ finishes at $20$ ps and waits idle. Two lessons
fall out of the table. First, **speeding up $g_2$ does nothing** — it is not on
the critical path — so effort spent there is wasted, a recurring mistake of
optimizing the average instead of the worst case. Second, if this block sits
between clocked registers, the clock period must exceed $120$ ps plus the
flip-flop overheads the [clocking lesson](/computer-architecture/digital-logic/memory-elements-latches-flip-flops-and-clocking)
adds. Every real timing tool is this table run over millions of gates.

## Don't-cares

Sometimes a truth table has rows you genuinely do not care about, because those
input combinations can never occur or the output is never used when they do.
Marking such rows as **don't-cares** (written $\times$) instead of forcing a $0$
or $1$ gives the synthesizer freedom, which translates into smaller circuits.

A concrete case: a 4-bit input $b_3 b_2 b_1 b_0$ holds a decimal digit $0$–$9$
(binary-coded decimal), and we want $f = 1$ when the digit is $5$ or more. The
six patterns $10$–$15$ can never arrive, so their rows are don't-cares:

| digit | $b_3 b_2 b_1 b_0$ | $f$ |
| --- | --- | --- |
| $0$–$4$ | `0000`–`0100` | $0$ |
| $5$–$9$ | `0101`–`1001` | $1$ |
| (unused) | `1010`–`1111` | $\times$ |

Forced to output $0$ on the unused rows, the minimal circuit is
$f = b_3\overline{b_2}\,\overline{b_1} + \overline{b_3} b_2 (b_1 + b_0)$;
it must carefully exclude the impossible patterns. Allowed to output anything on
them, the synthesizer can pick $1$ for the unused rows and the function collapses
to

$$
f = b_3 + b_2\,(b_1 + b_0),
$$

three gates instead of six or seven. The
[Karnaugh map](/computer-architecture/digital-logic/transistors-gates-and-boolean-functions)
shows why the don't-cares are worth so much. Lay $f$ out on a four-variable map,
$b_3 b_2$ down the side and $b_1 b_0$ across the top, both in Gray-code order, and
mark the six impossible patterns $\times$:

$$
% caption: The digit-at-least-5 function on a 4-variable K-map. The six impossible
% caption: patterns 10-15 are don't-cares (marked d). Grouped as 1s, they let the
% caption: whole bottom half (b3 = 1) merge into one term, and the two real 1s in
% caption: the b3 = 0 half give b2(b1 + b0), so f = b3 + b2(b1 + b0).
\begin{tikzpicture}[font=\footnotesize,>=stealth,
  cell/.style={draw, minimum size=8mm, inner sep=0pt}]
  \definecolor{acc}{HTML}{2348F2}
  \foreach \c/\lab in {0/{00}, 1/{01}, 2/{11}, 3/{10}}
    \node at (1.2+\c*0.8,3.35) {\lab};
  \node[anchor=east] at (0.7,3.35) {$b_1 b_0$};
  \foreach \r/\lab in {0/{00}, 1/{01}, 2/{11}, 3/{10}}
    \node[anchor=east] at (0.6,2.65-\r*0.8) {\lab};
  \node[anchor=east] at (-0.15,1.45) {$b_3 b_2$};
  % rows top->bottom: b3b2 = 00,01,11,10 ; cols b1b0 = 00,01,11,10
  % digit values: b3b2b1b0. f=1 for digits 5-9, x for 10-15.
  % row 00 (b3b2=00): digits 0,1,3,2 -> f 0,0,0,0
  \foreach \c/\v in {0/0,1/0,2/0,3/0} \node[cell] at (1.2+\c*0.8,2.65) {\v};
  % row 01 (b3b2=01): digits 4,5,7,6 -> f 0,1,1,1
  \node[cell] at (1.2+0*0.8,1.85) {0};
  \foreach \c in {1,2,3} \node[cell,fill=acc!8] at (1.2+\c*0.8,1.85) {1};
  % row 11 (b3b2=11): digits 12,13,15,14 -> all don't-care (d)
  \foreach \c in {0,1,2,3} \node[cell,fill=acc!8] at (1.2+\c*0.8,1.05) {d};
  % row 10 (b3b2=10): digits 8,9,11,10 -> f 1,1,d,d
  \node[cell,fill=acc!8] at (1.2+0*0.8,0.25) {1};
  \node[cell,fill=acc!8] at (1.2+1*0.8,0.25) {1};
  \node[cell,fill=acc!8] at (1.2+2*0.8,0.25) {d};
  \node[cell,fill=acc!8] at (1.2+3*0.8,0.25) {d};
  % b3 = 1 covers the whole bottom two rows (b3b2 = 11, 10)
  \draw[acc, thick] (0.72,-0.18) rectangle (3.88,1.48);
  \node[anchor=west, text=acc] at (4.05,0.65) {$b_3$};
\end{tikzpicture}
$$

The whole lower half of the map — the eight cells where $b_3 = 1$ — is now all
$1$s and $\times$s, so it merges into the single term $b_3$: an eight-cell block
naming one variable. The two remaining real $1$s in the $b_3 = 0$ half (digits
$5$–$7$, all with $b_2 = 1$ and at least one of $b_1, b_0$ set) contribute
$b_2 b_1$ and $b_2 b_0$, which factor to $b_2(b_1 + b_0)$. Read off the map:
$f = b_3 + b_2(b_1 + b_0)$. Without the don't-cares, that bottom-half block would
have holes at digits $10$–$15$ and could not merge, forcing the clumsier
five-literal expression. The freedom to fill impossible cells with whichever
value grows a block is precisely what buys the smaller circuit.

HCL has the same freedom built into its case expression below: the selects
you list are the cases you care about, and a final default arm sweeps up the
rest.

## HCL: describing logic as expressions

HCL lets you write a circuit as **expressions**, much like a programming language,
and a compiler maps the expressions onto gates. It has two kinds of signal. A
**bit-level** signal is a single wire carrying $0$ or $1$; a **word-level** signal
is a bundle of wires carrying an integer (in Y86-64, typically 64 bits wide).

The bit-level operators are written like C's **logical** operators but mean plain
single-bit gates, because every HCL bit signal is already one bit:

- `&&` is AND, `||` is OR, `!` is NOT.

So `s1 && !s0` is one AND gate fed by `s1` and an inverted `s0`. There is no
short-circuiting here: this is hardware, and both inputs to the AND gate exist as
wires at all times. A bit-level HCL expression is just a Boolean formula, and the
compiler realizes it as the corresponding gate network.

```c [bitlevel.hcl]
bool xor   = (a && !b) || (!a && b);   /* 1 when a, b differ */
bool eq    = !xor;                     /* 1 when a, b are equal */
bool maj   = (a && b) || (a && c) || (b && c);  /* majority of three */
```

The `bool` keyword declares a single-bit signal. These three lines describe the
XOR, equality, and majority circuits from the previous lesson: the same gates, now
as text a tool can synthesize.

## Word-level signals and equality nets

A word-level signal carries many bits at once; we write `word` for a 64-bit
signal. Comparisons between word signals produce a **bit** result, realized in
hardware as a tree of XNOR gates (per-bit equality) feeding one big AND.

```c [wordlevel.hcl]
word Reg;                   /* a 64-bit word signal */
bool isZero = (Reg == 0);   /* 1 iff every bit of Reg is 0 */
bool isRSP  = (rA == RRSP); /* register-id field equals the stack pointer id */
```

Equality `==`, inequality `!=`, and the ordering comparisons are all word-level
operators that **return a bit**. At the gate level `Reg == 0` asks "are all 64 bits
zero?" — a 64-input NOR — and `rA == RRSP` compares two 4-bit register-id fields
bit for bit. The point is that HCL hides the gate tree: you write the comparison,
the synthesizer builds the XNOR-and-AND network.

Names like `RRSP` follow a convention kept throughout the processor module:
capitalized HCL constants stand for fixed nibble values from the instruction
encoding. `IOPQ` is the `icode` of the `OPq` instructions (`6`), `RRSP` the
register id of `%rsp` (`4`), `RNONE` the "no register" id (`0xF`), `ALUADD` the
ALU's add function code (`0`). Writing `rA == RRSP` instead of `rA == 4` costs
nothing in hardware — both compile to the same comparator against a constant —
and it lets the control logic read as intent rather than magic numbers.

$$
% caption: What A == B costs in gates, shown for 4 bits. One XNOR per bit pair
% caption: answers "do these bits match?"; an AND tree combines the answers into a
% caption: single eq bit. A 64-bit compare is the same shape, six AND levels deep.
\begin{tikzpicture}[font=\footnotesize,>=stealth,
  g/.style={draw, minimum width=13mm, minimum height=7mm, inner sep=1pt}]
  \definecolor{acc}{HTML}{2348F2}
  \foreach \i/\y in {3/3.0, 2/2.0, 1/1.0, 0/0.0}{
    \node[g] (x\i) at (0,\y) {XNOR};
    \draw (-1.7,\y+0.18) node[anchor=east] {$a_\i$} -- (-0.65,\y+0.18);
    \draw (-1.7,\y-0.18) node[anchor=east] {$b_\i$} -- (-0.65,\y-0.18);
  }
  \node[g] (t1) at (2.6,2.5) {AND};
  \node[g] (t0) at (2.6,0.5) {AND};
  \node[g, fill=acc!8] (t2) at (5.2,1.5) {AND};
  \draw (x3.east) -- (1.4,3.0) -- (1.4,2.68) -- (1.95,2.68);
  \draw (x2.east) -- (1.4,2.0) -- (1.4,2.32) -- (1.95,2.32);
  \draw (x1.east) -- (1.4,1.0) -- (1.4,0.68) -- (1.95,0.68);
  \draw (x0.east) -- (1.4,0.0) -- (1.4,0.32) -- (1.95,0.32);
  \draw (t1.east) -- (4.0,2.5) -- (4.0,1.68) -- (4.55,1.68);
  \draw (t0.east) -- (4.0,0.5) -- (4.0,1.32) -- (4.55,1.32);
  \draw (t2.east) -- ++(1.1,0) node[anchor=west, text=acc] {eq};
\end{tikzpicture}
$$

The tree shape matters for speed: combining 64 bit-equalities pairwise takes
$\log_2 64 = 6$ AND levels, not 63: the same balanced-tree structure the fast
adder of the next lesson applies to carries.

One more word-level form appears constantly in the processor's control logic:
**set membership**, written `in`. The expression `icode in { IOPQ, IRRMOVQ }`
asks whether the word `icode` equals any member of the listed set, and it
compiles to exactly what that suggests — one equality comparator per member,
their outputs ORed together.

```c [membership.hcl]
bool need_regids =
    icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ, IIRMOVQ, IRMMOVQ, IMRMOVQ };
/* same circuit as: icode == IRRMOVQ || icode == IOPQ || ... */
```

A five-member test on a 4-bit field is five 4-bit comparators feeding a 5-input
OR — cheap, fixed-delay hardware. The processor module's case expressions are
guarded almost entirely by membership tests ("is this one of the instructions
that reads memory?"), and each one is just an OR of equality nets.

## The case expression is a multiplexer

The construct that carries most of HCL's weight is the **case expression**,
written with square brackets. It is a list of pairs, each a Boolean **select**
and a word-level **value**:

```c [case.hcl]
word Out = [
    s2 : A;      /* if s2 is true, Out = A      */
    s1 : B;      /* else if s1 is true, Out = B */
    1  : C;      /* else (default), Out = C     */
];
```

The semantics are **priority from top to bottom**: scan the selects in order, and
the value of the first one that is true becomes the result. A final select of `1`
is the catch-all default, guaranteeing some case always matches, and doubling as
the don't-care escape hatch: input combinations no listed select covers land in
the default arm, where any value that keeps the hardware simple will do. This is
**not** sequential "if/else" executed over time. It is a description of a
**multiplexer**, a combinational circuit that routes one of several data inputs to
the output according to the select signals. All the values $A$, $B$, $C$ exist on
wires simultaneously; the selects merely steer which one reaches `Out`.

$$
% caption: The case expression maps to a multiplexer tree. Each select drives a
% caption: 2:1 mux; top-of-list priority means the highest matching select wins.
% caption: With s2 true, A passes through regardless of s1.
\begin{tikzpicture}[font=\footnotesize,>=stealth,
  mux/.style={draw, minimum width=12mm, minimum height=14mm, inner sep=1pt,
              fill=acc!8}]
  \definecolor{acc}{HTML}{2348F2}
  % f\/irst mux: chooses between B and C using s1
  \node[mux] (m1) at (0,0) {MUX};
  \node[anchor=east] at (-1.6,0.35) {$B$};
  \node[anchor=east] at (-1.6,-0.35) {$C$};
  \draw (-1.6,0.35) -- (-0.6,0.35);
  \draw (-1.6,-0.35) -- (-0.6,-0.35);
  \node[anchor=north] at (0,-1.2) {$s_1$};
  \draw (0,-1.15) -- (0,-0.7);
  % second mux: chooses between A and (output of m1) using s2
  \node[mux] (m2) at (3.4,0.35) {MUX};
  \node[anchor=east] at (1.9,0.7) {$A$};
  \draw (1.9,0.7) -- (2.8,0.7);
  \draw (m1.east) -- (2.8,0.0);
  \node[anchor=north] at (3.4,-1.2) {$s_2$};
  \draw (3.4,-1.15) -- (3.4,-0.35);
  \draw (m2.east) -- ++(1.4,0) node[anchor=west,text=acc] {\texttt{Out}};
\end{tikzpicture}
$$

The priority ordering matters: in the figure, if $s_2$ is true the top mux passes
$A$ through and the value of $s_1$ is irrelevant, exactly matching "the first true
select wins." When the selects are mutually exclusive (at most one true at a time)
the priority is harmless and the case reads like a plain table; when they can
overlap, the top-down rule resolves the conflict. This single construct expresses
nearly every control decision in a processor — which result the ALU produces, which
register to write, whether to take a branch — and the next lesson shows the
multiplexer it compiles to in full gate detail.

## From HCL to industrial HDLs

HCL is a deliberately tiny language — bit and word signals, `&& || !`, comparisons,
`in`, and `case`. It exists to make the Y86-64 control logic readable, not to tape
out a chip. The industrial languages it stands in for are worth knowing.

**Verilog, VHDL, SystemVerilog.** Real hardware is described in one of these. A
combinational block that HCL writes as a `case` expression appears in Verilog as a
`always @(*)` block with a `case` statement, or as a chain of continuous
`assign`s; the synthesizer infers the same mux tree. The discipline these
languages enforce, which HCL sidesteps by construction, is the split between
**combinational** and **sequential** code. Assign a signal in an edge-sensitive
block (`always @(posedge clk)`) and you get a flip-flop; assign it combinationally
and you get gates. Forget to cover a case — leave a combinational signal
unassigned on some path — and the tool infers an unwanted **latch**, a classic bug
the linters warn about, and exactly the transparency hazard the
[next-lesson clock discipline](/computer-architecture/digital-logic/memory-elements-latches-flip-flops-and-clocking)
is built to avoid. SystemVerilog added `always_comb` and `always_ff` precisely so
the designer states which one they mean and the tool checks it.

**The two-language problem and its escapes.** Verilog and VHDL describe hardware at
the register-transfer level: you specify every register and the logic between them.
Newer flows raise the altitude. **Chisel** (Bachrach et al., 2012), a hardware
construction language embedded in Scala, lets a generator emit families of circuits
parametrically and compiles down to Verilog; it is what the open **RISC-V** Rocket
and BOOM cores are written in. **High-level synthesis** goes further, compiling a
restricted subset of C or C++ directly to RTL, trading designer control for
productivity on datapath-heavy blocks. All of them still bottom out in the gates,
muxes, and critical-path arithmetic of this lesson — the abstraction rises, the
physics does not. The critical path is still the sum of gate delays on the worst
route, and closing timing on it is still the central chore of every tape-out.

> **Takeaway.** Combinational logic computes a pure function of its current inputs
> with no feedback and no memory; sequential logic (next lessons) adds state. Its
> outputs are trustworthy only after the **critical path** — the slowest chain of
> gate delays — has settled, and that number bounds the clock. Don't-care rows buy
> the synthesizer smaller circuits. HCL describes combinational circuits as text:
> bit-level `&& || !` are single gates, word-level `==`/`!=` (and the set test
> `in`, an OR of equality nets) return a bit, and the bracketed `case` expression
> is a **priority multiplexer**
> — the first true select picks the output.

Next we build the [multiplexers, decoders, and the ALU](/computer-architecture/digital-logic/multiplexers-decoders-and-the-alu)
that these expressions stand for.
