The Computing Series

Logical Operators

Introduction

The bug was in production for eleven months before anyone found it.

A payments platform had an authorization check:

if not user.is_suspended or user.has_override:
    process_payment(user, amount)

The intent was: “process if the user is not suspended, OR if they have a manual override.” The code reads like that intent. But it does not implement it.

The correct logic is: “process if (not suspended) OR (has override).” The code reads not user.is_suspended or user.has_override, which, by operator precedence, evaluates as (not user.is_suspended) or (user.has_override).

Those are the same thing. So where is the bug?

The bug was in a different function — one that computed is_suspended by combining two flags:

# Original intent: suspended if account_frozen AND payment_blocked
user.is_suspended = account_frozen and payment_blocked

A developer needed to change the policy: “suspended if account_frozen OR payment_blocked.” They updated the assignment:

user.is_suspended = account_frozen or payment_blocked

Then the authorization check, which negated is_suspended, silently changed meaning. The original check read: “not (frozen AND blocked).” By De Morgan’s Law, this equals “(not frozen) OR (not blocked)”, which is True for almost every user. The intent was never this permissive. Accounts that were frozen but not blocked could process payments.

Eleven months. The fix was one line. The damage was regulatory.

De Morgan’s Laws are not an academic curiosity. They are the source of real authorization failures.


Thread Activation

Thread 7 (State Machines) continues here from Chapter 1.

Chapter 1 established the atomic unit — a single statement with a truth value. This chapter adds the connectives that combine atomic statements into compound ones. Compound statements are the building blocks of compound state: a system that is simultaneously in multiple conditions. A request is authenticated AND authorized AND rate_not_exceeded. A distributed node is leader AND quorum_reached AND log_committed. Every compound state in every system you will build is a logical expression over atomic propositions.

The direction is: atomic propositions (Ch 1) → compound state (this chapter) → compound state in state machines (Ch 13) → stateless design in distributed services (Book 3, Ch 3).

The backward link: this chapter also activates Thread 12 (Tradeoffs). The exponential complexity introduced by combining boolean variables — the subject of Chapter 3 — is a direct consequence of the combinatorial space opened by logical operators.


The Concept

Logical operators combine statements into new statements. The four fundamental operators are:

Operator Symbol Name Evaluates to True when…
AND Conjunction Both operands are True
OR Disjunction At least one operand is True
NOT ¬ Negation The operand is False
XOR Exclusive Or Exactly one operand is True

These are complete definitions. There is no ambiguity. Each operator is fully defined by its truth table — a table listing every combination of input values and the resulting output. Truth tables are Chapter 3’s subject. For now, the definitions above are sufficient.

AND

P AND Q is True if and only if both P is True and Q is True. One False operand makes the whole expression False.

In programming: if user.is_authenticated and user.has_permission("write"): — access is granted only when both conditions hold.

OR

P OR Q is True if at least one of P, Q is True. Both can be True simultaneously. This is inclusive OR — it includes the case where both are True.

In programming: if request.is_cached or fallback_available: — service continues if either condition holds.

NOT

NOT is unary — it takes one operand and inverts its truth value. True becomes False; False becomes True.

In programming: if not user.is_banned: — the inverse of the stored condition.

XOR

XOR is True when exactly one operand is True. It is False when both are True and False when both are False. XOR is less common in high-level boolean logic, but it appears in:

  • Cryptography: bitwise XOR for key mixing and encryption
  • Error detection: parity bits in RAID-5 are computed with XOR
  • Toggle logic: state = state XOR True flips a boolean without an if-statement
# XOR implemented in Python (Python has no boolean XOR keyword, use !=)
def xor(p: bool, q: bool) -> bool:
    # XOR: exactly one of p, q must be True
    return p != q

# Alternatively, explicit:
def xor_explicit(p: bool, q: bool) -> bool:
    return (p or q) and not (p and q)

How It Works

De Morgan’s Laws

De Morgan’s Laws are the most important identities in boolean algebra for working programmers.

Law 1: NOT (P AND Q) = (NOT P) OR (NOT Q)

Law 2: NOT (P OR Q) = (NOT P) AND (NOT Q)

In plain language:

  • To negate a conjunction, distribute the negation and change AND to OR.
  • To negate a disjunction, distribute the negation and change OR to AND.

The transformation is symmetric. If you negate the result of applying the law, you get back the original.

Why this matters in code:

Programmers frequently write conditions as negative checks. “Don’t allow if suspended or banned.” In code:

# Version A: direct negation of compound condition
if not (user.is_suspended or user.is_banned):
    allow_access()

# Version B: De Morgan's applied manually
if not user.is_suspended and not user.is_banned:
    allow_access()

These are equivalent. A and B evaluate identically for every combination of is_suspended and is_banned. You can verify this with a truth table (Chapter 3 will formalize this). For now, trust the law and verify with a few cases:

is_suspended is_banned A result B result
False False True True
True False False False
False True False False
True True False False

Identical. The laws are correct.

Where De Morgan’s fails programmers:

The failure is not in the law. The law is provably correct. The failure is in applying the law incorrectly while refactoring.

Consider the payments bug from the opening hook. The original condition was computed as:

is_suspended = account_frozen and payment_blocked  # True only when BOTH hold

The authorization check was:

if not is_suspended:  # allow if NOT (frozen AND blocked)
    # By De Morgan: allow if (NOT frozen) OR (NOT blocked)
    # This is True for almost everyone
    process_payment()

The developer changed is_suspended to account_frozen or payment_blocked, intending to catch either condition. Now:

is_suspended = account_frozen or payment_blocked  # True if EITHER holds

But the authorization check still reads not is_suspended, which now means NOT (frozen OR blocked), which by De Morgan’s equals (NOT frozen) AND (NOT blocked). This is stricter than before: only accounts that are neither frozen nor blocked can process payments. That stricter behavior may well be what the developer intended for the new policy, but it is not what callers expected. Code written against the old definition of is_suspended (AND semantics, permissive) now silently operates under the new definition (OR semantics, strict). The real failure is that the intermediate variable changed meaning without any visible signal — its name, type, and call site all remained identical. The contract (FM8) broke without an error being raised. Any system that depended on the old behavior — including callers that deliberately relied on frozen-but-not-blocked accounts being allowed through — now behaves differently, silently and immediately.

# Safer pattern: name the authorization condition explicitly
def can_process_payment(account_frozen: bool, payment_blocked: bool) -> bool:
    # Explicit statement: account is usable for payments
    # Named conditions prevent silent semantic drift
    account_is_usable = not account_frozen and not payment_blocked
    return account_is_usable

By naming the terminal condition, you eliminate the intermediate is_suspended variable and the implicit De Morgan inversion.

Short-Circuit Evaluation

Most programming languages implement short-circuit evaluation for AND and OR. This is a performance and safety optimization derived from the truth table structure.

Short-circuit AND:

If the left operand of P AND Q is False, the whole expression is False. The right operand does not need to be evaluated. Python, Java, C, JavaScript — all short-circuit AND.

def get_user_role(user_id: int, db) -> str | None:
    # Short-circuit AND: if user_id is falsy, db.get() is never called
    # This prevents a database call with an invalid ID
    user = user_id and db.get_user(user_id)
    return user.role if user else None

# More common pattern: guard clause using short-circuit
def process(items: list, transform) -> list:
    # If items is empty, transform is never called
    return items and [transform(item) for item in items]

Short-circuit OR:

If the left operand of P OR Q is True, the whole expression is True. The right operand is not evaluated.

def get_config_value(key: str) -> str:
    # Short-circuit OR: use cached value if available, otherwise fetch
    # fetch_from_remote() is only called if cache returns None/falsy
    return config_cache.get(key) or fetch_from_remote(key)

Implications for side effects:

Short-circuit evaluation means the right operand might not run. If the right operand has side effects — increments a counter, logs a message, modifies state — those side effects are conditional on the left operand’s value.

# Dangerous: side effect in right operand
# record_attempt() may or may not run depending on is_enabled
if is_enabled and record_attempt(user_id):
    grant_access()

If is_enabled is False, record_attempt is never called. If audit logging is required for all access attempts (including denied ones), this code silently skips the log. This is FM11 (Observability Blindness) in miniature — the system fails to record events it should be recording.

Non-short-circuit evaluation:

When you need both operands to evaluate regardless of the first, avoid compound boolean expressions with side-effectful operands. Call each function separately and store the result:

# Safe: both functions always called
attempt_recorded = record_attempt(user_id)    # always runs
is_enabled = check_enabled(user_id)            # always runs

if is_enabled and attempt_recorded:
    grant_access()

Operator Precedence

In code, NOT binds tighter than AND, which binds tighter than OR. This matches mathematical convention.

NOT > AND > OR

not a or b and c parses as (not a) or (b and c).

Explicit parentheses eliminate ambiguity. The recommendation is simple: parenthesize compound conditions. The performance cost is zero. The readability cost is zero. The correctness benefit is real.

# Ambiguous: relies on precedence knowledge
if not is_suspended or has_override and is_premium:
    ...

# Unambiguous: explicit structure
if (not is_suspended) or (has_override and is_premium):
    ...

Truth Table Preview

Each operator’s behavior is fully captured by a 2-variable truth table. Chapter 3 builds these systematically. For reference:

P     Q     P AND Q   P OR Q   NOT P   P XOR Q
True  True  True      True     False   False
True  False False     True     False   True
False True  False     True     True    True
False False False     False    True    False

Tradeoffs

AT3 — Simplicity vs. Flexibility

Four operators: AND, OR, NOT, XOR. This is a minimal, complete set. In fact, AND and NOT alone are functionally complete — every possible boolean function can be expressed using only AND and NOT. NAND (NOT AND) is also functionally complete by itself. Hardware designers use NAND gates extensively because NAND is cheap to fabricate and expressive enough to build everything else.

The simplicity is the point. Four operators. Fixed truth tables. No ambiguity. The cost of this simplicity is that complex real-world conditions require verbose expressions.

“Allow access if the user is an admin, or if they own the resource and either the resource is public or they have an explicit grant, but not if their account is suspended regardless of ownership.”

This translates to:

def can_access(user: User, resource: Resource) -> bool:
    # Atomic statements
    is_admin = user.role == "admin"
    owns_resource = resource.owner_id == user.id
    resource_is_public = resource.visibility == "public"
    has_explicit_grant = user.id in resource.explicit_grants
    is_suspended = user.account_status == "suspended"

    # Compound statement: access rule
    base_access = is_admin or (owns_resource and (resource_is_public or has_explicit_grant))

    # Override: suspension blocks all access
    return base_access and not is_suspended

The verbosity is intentional. Each named variable is an atomic statement. The compound statement is built from named parts. The structure is readable, testable, and debuggable. The alternative — one long inline expression — is flexible (fewer lines) but less simple to reason about. AT3 applies: the named-variable approach favors simplicity of reasoning; the inline approach favors brevity.


Where It Fails

FM4 — Data Consistency Failure

Misapplying De Morgan’s Laws causes inconsistent boolean evaluation across system boundaries.

The canonical failure: a server enforces an access rule using one boolean expression. A client enforces the same rule using a supposedly equivalent expression. The expressions are not actually equivalent — De Morgan’s was applied incorrectly. The server allows requests the client thinks it blocked. The inconsistency is undetected until a security audit.

# Server: allow if not (read_only and no_write_grant)
# Intent: "block only if BOTH conditions hold"
server_allows = not (user.is_read_only and not user.has_write_grant)
# By De Morgan: (not is_read_only) OR (has_write_grant)

# Client: blocks if read_only OR no write grant
# Intent: "block if EITHER condition holds"
client_blocks = user.is_read_only or not user.has_write_grant
# client_allows = not client_blocks = (not read_only) AND (has_write_grant)

server_allows and client_allows are different. A user who is read-only but has a write grant is allowed by the server (because has_write_grant is True, the OR fires) but blocked by the client (because is_read_only is True, the AND fails). The server and client disagree. Requests that reach the server get processed. The client never sends them. Or worse: a different client sends them directly.

The prevention is shared predicate functions. Define the access rule once. Call it from both sides.

# Shared predicate: single source of truth for the access rule
def user_can_write(user: User) -> bool:
    # Explicit statement: user is permitted to write
    return not user.is_read_only or user.has_write_grant

# Server and client both call user_can_write(user)
# They cannot disagree because they call the same function

Real Systems

Access Control Lists — UNIX file permissions are AND/OR/NOT over user, group, and other permission bits. chmod 755 sets specific bits; ls -l reads them back as boolean flags. The execute bit for a directory means “can traverse” — a compound access decision ANDs the traverse bit with the parent directory’s bits recursively.

Kubernetes Pod Scheduling — a Pod’s scheduling constraints are boolean expressions. nodeAffinity expressions use AND/OR to combine requirements. A node must match all required terms (AND) and any of the preferred terms (OR). The scheduler evaluates these expressions for every candidate node. An incorrectly specified affinity expression causes Pods to be scheduled to wrong nodes or not scheduled at all.

Firewall Rules — iptables and cloud security groups evaluate packets against chains of rules, each of which is a conjunction (AND) of match criteria: source IP AND destination port AND protocol. The rule matches only when all criteria match. De Morgan’s appears when rules are negated: “block all traffic except from 10.0.0.0/8” is implemented as two rules because negation distributes differently over sets of criteria.

Circuit Breakers — a circuit breaker opens (stops traffic) when failure_rate > threshold AND window_is_active. It resets (allows traffic) when probe_succeeds OR reset_timeout_expired. These are literal boolean expressions evaluated by the circuit breaker at each request. Short-circuit evaluation applies: if failure_rate > threshold is False, window_is_active may not be checked.

SQL WHERE Clauses — every SQL WHERE clause is a boolean expression over column predicates. The query optimizer applies De Morgan’s Laws and boolean identities to rewrite the expression into a form that can use indexes efficiently. WHERE NOT (a = 1 AND b = 2) may be rewritten as WHERE a != 1 OR b != 2 if that form better exploits available indexes. The optimizer is an automated De Morgan’s transformer.


Concept: Logical Operators (AND, OR, NOT, XOR) and De Morgan’s Laws

Core Idea: AND, OR, NOT, and XOR combine atomic statements into compound ones. De Morgan’s Laws define how negation distributes over AND and OR — misapplying them is a consistent source of authorization bugs.

Tradeoff: AT3 — Simplicity vs. Flexibility (four operators are minimally sufficient but verbose for complex conditions; named atomic variables trade brevity for clarity)

Failure Mode: FM4 — Data Consistency Failure (two components applying De Morgan’s independently can evaluate the same rule differently)

Signal: When an authorization check produces unexpected results, or when a NOT distributes over a compound condition during refactoring — check De Morgan’s. Draw the truth table.


Exercises

Level 1 — Understand

  1. Apply De Morgan’s Law to simplify each expression. Show each step.
    • NOT (user_is_admin OR user_is_owner)
    • NOT (request_is_valid AND session_is_active AND rate_not_exceeded)
    • NOT (NOT a AND NOT b)
  2. Given P = True and Q = False, evaluate:
    • P AND Q
    • P OR Q
    • NOT P OR Q
    • NOT (P AND NOT Q)
    • P XOR Q
  3. What is short-circuit evaluation? Give one situation where it prevents a bug and one situation where it introduces a bug.

Level 2 — Apply

A rate limiter has this condition:

def should_allow(request_count: int, limit: int, has_premium: bool, is_admin: bool) -> bool:
    return (request_count < limit or has_premium) and not is_admin
  1. The developer meant: “allow if under limit or premium, but always block admins (they use a separate endpoint).” Is the code correct? Evaluate for request_count=100, limit=50, has_premium=False, is_admin=False.

  2. A new requirement: “never block if admin.” The developer changes not is_admin to is_admin. Evaluate the same inputs. What changed?

  3. The actual requirement is: “allow if under limit OR has_premium, regardless of admin status — admin is handled elsewhere.” Rewrite the function to match this intent and remove the admin check entirely. Name the AT3 tradeoff involved.

Level 3 — Design

An e-commerce platform has this access rule, stated in English:

“A user can modify an order if they placed the order and the order is not yet shipped, OR if they are a support agent and the order has an open dispute, OR if they are an admin.”

  1. Identify every atomic statement in this rule.

  2. Write a Python function can_modify_order(user, order) -> bool using named boolean variables for each atomic statement.

  3. Write the De Morgan’s-equivalent formulation of the first clause only: “(placed the order) AND (not yet shipped)”. What does the De Morgan’s form express in plain language? Is it useful?

  4. Suppose the rule is enforced on both the frontend (React) and backend (Python API). Identify two specific ways FM4 (Data Consistency Failure) can occur, and propose a mitigation that uses the AT8 (Coupling vs. Cohesion) tradeoff explicitly.

A complete answer will: define the chosen approach, quantify its cost or complexity, and identify what breaks first as scale or load increases.

Read in the book →