---
# System prepended metadata

title: A bit of logic

---

# If You Can't Enumerate the Cases, You're Guessing

**STATUS**: draft draft daft!

## Stop Guessing About Boolean Logic

The most common bugs I see from refactoring aren't algorithmic. They come from this: "these conditions look the same; let's merge them." Sometimes you can. Sometimes you can't. The problem is knowing which case you're in. Sometimes, we have to be clinical not intuitive.

What follows are two small tools that make that decision mechanical: truth tables (for verifying whether predicates are actually equivalent) and [De Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan%27s_laws) (for rewriting predicates into a form we can read). They're basic. They're also underused in day-to-day engineering. I want to show how they work on real-ish examples, because the gap between "I've heard of truth tables" and "I reach for one when I'm staring at a suspicious refactor" is surprisingly wide. Chris, if you're reading this, and I'm sure you are because I will have pestered you; thanks for the inspiration and nudge to tackle this explanation.


## Ground rules

Before we jump into examples, let's pin down three terms we'll use throughout:

1. **Predicate**: a boolean-valued function that encodes a decision boundary. `user.isAdmin`, `kind.is_fn_ptr()`, `form.hasTitle && !form.isSaving`; these are all predicates.
2. **Truth table**: an exhaustive mapping from every combination of predicate values to outcomes. It makes implicit logic explicit; there's nowhere for edge cases to hide.
3. **Enumerating predicates**: turning implicit branching logic into a finite, explicit set of boolean variables so we can reason about them systematically.

---

So: two predicates, `A` and `B`. If a system's behavior depends on both, how many input combinations do we need to account for? Each predicate has 2 possible values, so the total is 2² = 4:

| A | B |
|:---:|:---:|
| 0 | 0 |
| 0 | 1 |
| 1 | 0 |
| 1 | 1 |

---

Now that we've laid out the input space, what do the operations actually produce? Here are the three compositions that come up most often: AND, OR, and XOR.

(We'll use standard [boolean algebra](https://en.wikipedia.org/wiki/Boolean_algebra) notation for the formulas: `¬` is negation (`!`), `∧` is AND (`&&`), `∨` is OR (`||`), `⊕` is XOR. The code examples use the code-syntax equivalents; the math notation appears only in the algebraic reductions.)

| A | B | A ∧ B | A ∨ B | A ⊕ B |
|:---:|:---:|:-----:|:-----:|:-----:|
| 0 | 0 |   0   |   0   |   0   |
| 0 | 1 |   0   |   1   |   1   |
| 1 | 0 |   0   |   1   |   1   |
| 1 | 1 |   1   |   1   |   0   |

That's the whole toolkit. Everything below is just application.


## 1. "Looks Equivalent" Is Not Equivalent

### The situation

Here's a function you've inherited:

```javascript
function canEditDocument(user) {
  if (user.isAdmin || user.isOwner) {
    return true;
  }

  if (user.hasEditGrant && !user.isSuspended) {
    return true;
  }

  return false;
}
```

Two branches. Both return `true`. Both "grant edit access." A natural instinct: collapse them.

```javascript
function canEditDocument(user) {
  return (
    (user.isAdmin || user.isOwner || user.hasEditGrant) &&
    !user.isSuspended
  );
}
```

Shorter. Cleaner. Also wrong.

But here's the thing: it's difficult what's wrong by inspection. It *looks* right. The bug only becomes visible when you stop reading and start enumerating.

### The approach

Name the predicates so we can reason about them without getting lost in the syntax:

- `P` = `isAdmin || isOwner` (privileged editor)
- `G` = `hasEditGrant`
- `S` = `isSuspended`

The original logic is: `P || (G && !S)`

The "simplified" version is: `(P || G) && !S`

Worth pausing on the parentheses here, because they're doing all the work. In boolean algebra (just like arithmetic), precedence matters. The hierarchy in code is `!` > `&&` > `||` (the mnemonic is "NOT tightens, AND binds, OR loosens"). In arithmetic: `×`, `/` > `+`, `-`. Same idea: the "multiplication-like" operator evaluates before the "addition-like" one.

If you've ever been bitten by `2 + 3 × 4` evaluating to `14` (not `20`), this is the same trap in boolean form. A concrete example you can verify in a REPL:

```javascript
const result = true || false && false;
// Actual:    true || (false && false) → true
// Wrong assumption: (true || false) && false → false
```

Same terms, completely different result depending on which operator binds first. The arithmetic version is even easier to see: `5 + (3 × 0)` is `5` (the multiplication is scoped; the `5` stands alone), but `(5 + 3) × 0` is `0` (the zero applies to everything).

That's exactly what's happening with our predicates. The two expressions parse into different trees:

```
Original: P || (G && !S)       Refactor: (P || G) && !S

        OR                            AND
       /  \                          /   \
      P   AND                       OR   !S
         /   \                     /  \
        G    !S                   P    G
```

Same symbols, different shape. In the original tree, `!S` only applies to the G branch; `P` exits to `true` independently. In the refactored tree, `!S` sits at the root and applies to *everything*, including the P path. The refactor didn't rearrange terms; it changed the structure of the expression. If you're not tracking operator precedence carefully, the code looks like it's just moving things around; the trees show that it's changing *what gets negated by suspension*.

(Practical advice: even if you know the precedence rules, use explicit parentheses when the logic matters. `return (isAdmin || isOwner) && !isSuspended` is not redundant; it's communication. You're not writing for the compiler; you're writing for the next engineer, which is often you in two weeks.)

These look similar. Are they equivalent? We don't guess. We enumerate.

- let Orig = `P || (G && !S)`
- let Ref  = `(P || G) && !S`

| P | G | S | Orig| Ref | ≡ |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | ✓ |
| 0 | 0 | 1 | 0 | 0 | ✓ |
| 0 | 1 | 0 | 1 | 1 | ✓ |
| 0 | 1 | 1 | 0 | 0 | ✓ |
| 1 | 0 | 0 | 1 | 1 | ✓ |
| **1** | **0** | **1** | **1** | **0** | **✗** |
| 1 | 1 | 0 | 1 | 1 | ✓ |
| **1** | **1** | **1** | **1** | **0** | **✗** |

The rows where `P=1` and `S=1` are the problem: a privileged user who is suspended. The original returns `true`; the refactor returns `false`. The "simplification" silently changed what suspension means for admins and owners.

The duplication wasn't accidental. It encoded a real asymmetry: admins and owners bypass suspension; grant-based editors do not. The two branches *looked* like equivalent rules, but they were actually two **different** rules that changed the branching outcome.

### The principle

**Truth tables turn intuition into proof.** The real question when deduplicating branching logic isn't "can we merge these?" It's: *do the predicates produce the same result for every possible input?* That's not something to reason about informally. Enumerate the input space; tabulate the outputs side by side. If they agree everywhere, merge freely. If they disagree somewhere, the disagreeing rows tell you exactly what the refactor would break.

So what does the correct refactor look like? The truth table tells us: we need to preserve the asymmetry. Privileged editors get one rule; grant-based editors get another. Name the variation, keep it explicit:

```javascript
function canEditDocument(user) {
  const isPrivileged = user.isAdmin || user.isOwner;
  const hasConditionalGrant = user.hasEditGrant && !user.isSuspended;

  return isPrivileged || hasConditionalGrant;
}
```

No duplication. No loss of meaning. The table didn't just catch the bug; it told us how to structure the fix.

### A practical loop for safe deduplication

When collapsing similar branches:

1. **Name the predicates.** Give each boolean condition a short variable name so you can reason about the logic, not the syntax.
2. **Enumerate the input space.** Build a truth table. List every combination of predicate values.
3. **Compare outputs side by side.** Add a column for each version of the logic. Add an equivalence column.
4. **If they differ, identify the variation.** The disagreeing rows tell you exactly which inputs would break under a naive merge.
5. **Make that variation explicit in the code.** Name the variation point; keep the shared logic shared.

If you skip step 2, you're guessing.


## 2. De Morgan's Law: Make Predicates Readable

### The situation

Truth tables help us decide whether two predicates are the same. De Morgan's laws help us rewrite a single predicate into a form we can actually reason about. Different problem, complementary tool.

Here's a function that works correctly but is unpleasant to read:

```javascript
function isSubmitEnabled(form) {
  return !(!form.hasTitle || !form.hasEmail || form.isSaving);
}
```

This is the kind of thing people read three times and still don't trust. The logic is: "it's NOT the case that (title is missing OR email is missing OR we're currently saving)." Correct, but written in "reasons to reject" form, behind a negation. The reader has to mentally invert a disjunction of negations, which is one of those operations we're bad at. A side note, I cry a little every time I have to do this exercise.

### The approach

Name the pieces:

- `T` = `form.hasTitle`
- `E` = `form.hasEmail`
- `S` = `form.isSaving`

The expression is: `!((!T) || (!E) || S)`

De Morgan's law says: `!(X || Y || Z)` = `(!X) && (!Y) && (!Z)`

Apply it:

```
!((!T) || (!E) || S)
= T && E && !S
```

Rewrite:

```javascript
function isSubmitEnabled(form) {
  return form.hasTitle && form.hasEmail && !form.isSaving;
}
```

Same logic. Now it reads as a list of requirements ("title exists, email exists, not currently saving") instead of a negated list of rejections. That's a better predicate for both maintenance and review, because the conditions map directly to what we'd say if asked "when is submit enabled?"

### A more realistic example

```javascript
function canPublish(post) {
  return !(
    post.tags.length === 0 ||
    post.title.trim() === "" ||
    post.errors.some(e => e.severity === "fatal")
  );
}
```

Apply De Morgan:

```javascript
function canPublish(post) {
  return (
    post.tags.length > 0 &&
    post.title.trim() !== "" &&
    !post.errors.some(e => e.severity === "fatal")
  );
}
```

The rewrite is easier to extend (add a new requirement: just add another `&&` clause), easier to test (each clause is independently verifiable), and easier to reason about (the conditions read top to bottom as "what does the post need?").

### The principle

**De Morgan's laws convert negated compound conditions into an easier-to-read form.** The mechanical rule is simple: push the negation inward, flip `||` to `&&` (or vice versa), negate each term. The payoff is that you go from "it's NOT the case that (A OR B OR C)" to "not-A AND not-B AND not-C," which almost always maps more naturally to how most people (at least me) think about the logic.

A small example that's almost toy-sized but memorable:

```javascript
if (!(user.isActive && user.hasPassword)) {
  denyLogin();
}
```

De Morgan: `!(A && B)` = `!A || !B`

```javascript
if (!user.isActive || !user.hasPassword) {
  denyLogin();
}
```

That second form is often better when you want to log *why* login failed, because you can split the cases afterward. The negated conjunction hides the individual failure modes; the disjunction exposes them.


## 3. When to Reach for Each Tool

These two techniques solve different problems. It's worth being explicit about which one to reach for:

| Situation | Tool | What it answers |
|-----------|------|-----------------|
| Merging or deduplicating branches | Truth table | Are these predicates actually equivalent? |
| Verifying a refactor preserves behavior | Truth table | Does the new version agree on every input? |
| Something "feels equivalent" but you're not sure | Truth table | Where exactly do they disagree? |
| A predicate is full of negations | De Morgan | Can we rewrite this into positive form? |
| Logic is correct but hard to read | De Morgan | Can we express requirements instead of rejections? |
| Code review: "I can't tell what this condition does" | De Morgan | What does this actually say in plain English? |

Truth tables answer: *can we merge these branches safely?*

De Morgan answers: *can we rewrite this condition into a clearer shape?*

Both tools make our reasoning explicit. That matters in systems work, because a missed edge case becomes a production incident, a "harmless" refactor changes behavior under rare inputs, and duplicated logic diverges over time.

So: next time the code looks right but you don't quite trust it, or a refactor seems obvious but carries risk, or two conditions seem identical but something feels off; reach for these.

Truth tables give you a complete model. De Morgan gives you a readable one.

If you can't enumerate the cases, you're guessing.

I created a [repo for you to practice these concepts](https://github.com/cds-io/logic-exercises). Happy learning!
