Skip to content
The Lumenary

Math laboratory

Church numerals, recursion and non-termination in the λ-calculus

Parses named λ-terms into de Bruijn indices and reduces them with capture-avoiding substitution. Normal-order reduction computes Church arithmetic (2+3, 2*3, 2^3), booleans, Kleene's predecessor and 3! via the Y combinator, counting β-steps; Omega exhausts any fuel, and K I Omega normalises under normal order but not applicative order. Exact results for these terms only, not proofs of the general theorems.

Back to the labExact computation
Church numerals, recursion and non-termination in the λ-calculusPythonExact computationComputationally tested
"""A small lambda-calculus interpreter: Church numerals, recursion, and loops.

Terms are written in the usual named syntax (``\\x. x`` or ``λx. x``), parsed
into de Bruijn indices, and reduced with capture-avoiding substitution. Normal
order (leftmost-outermost redex first) computes Church-numeral arithmetic,
booleans, Kleene's predecessor and a factorial defined with the fixed-point
combinator Y, counting beta-steps. Omega = (λx. x x)(λx. x x) runs out of fuel,
and K I Omega shows that the strategy matters: normal order finds the normal
form, applicative order (arguments first) never does. Every result is an exact
computation for these particular terms.
"""

import re

# A term is ("var", index), ("app", function, argument) or ("lam", body).

def parse(text, defs=None):
    """Parse a named λ-term into de Bruijn form.

    Syntax: ``\\x. body`` or ``λx. body`` (abstraction extends to the right),
    application by juxtaposition (left-associative), parentheses, and names
    from ``defs`` (a dict name -> already-parsed closed term) used as macros.
    """
    defs = defs or {}
    tokens = re.findall(r"[\\λ.()]|[A-Za-z0-9_']+", text)
    pos = 0

    def peek():
        return tokens[pos] if pos < len(tokens) else None

    def take(expected=None):
        nonlocal pos
        tok = peek()
        if tok is None or (expected is not None and tok != expected):
            raise ValueError(f"expected {expected!r} at token {pos}: {text!r}")
        pos += 1
        return tok

    def term(scope):
        if peek() in ("\\", "λ"):
            take()
            name = take()
            take(".")
            return ("lam", term([name] + scope))
        result = None
        while peek() not in (None, ")"):
            arg = atom(scope)
            result = arg if result is None else ("app", result, arg)
            if peek() in ("\\", "λ"):
                arg = term(scope)
                result = ("app", result, arg)
        if result is None:
            raise ValueError(f"empty term: {text!r}")
        return result

    def atom(scope):
        tok = take()
        if tok == "(":
            inner = term(scope)
            take(")")
            return inner
        if tok in scope:
            return ("var", scope.index(tok))
        if tok in defs:
            return defs[tok]
        raise ValueError(f"unbound name {tok!r} in {text!r}")

    result = term([])
    if pos != len(tokens):
        raise ValueError(f"unexpected {peek()!r} in {text!r}")
    return result

def shift(t, d, cutoff=0):
    """Add ``d`` to every free index (those >= ``cutoff``)."""
    if t[0] == "var":
        return ("var", t[1] + d) if t[1] >= cutoff else t
    if t[0] == "app":
        return ("app", shift(t[1], d, cutoff), shift(t[2], d, cutoff))
    return ("lam", shift(t[1], d, cutoff + 1))

def subst(t, j, s):
    """Capture-avoiding substitution t[s/j]: the shift under λ avoids capture."""
    if t[0] == "var":
        return s if t[1] == j else t
    if t[0] == "app":
        return ("app", subst(t[1], j, s), subst(t[2], j, s))
    return ("lam", subst(t[1], j + 1, shift(s, 1)))

def beta(body, arg):
    """Contract the redex (λ. body) arg."""
    return shift(subst(body, 0, shift(arg, 1)), -1)

def step_normal(t):
    """One normal-order step (leftmost-outermost redex), or None if normal."""
    if t[0] == "var":
        return None
    if t[0] == "lam":
        b = step_normal(t[1])
        return None if b is None else ("lam", b)
    f, a = t[1], t[2]
    if f[0] == "lam":
        return beta(f[1], a)
    f2 = step_normal(f)
    if f2 is not None:
        return ("app", f2, a)
    a2 = step_normal(a)
    return None if a2 is None else ("app", f, a2)

def step_applicative(t):
    """One applicative-order step: reduce inside the function and argument
    first, and contract a redex only when both parts are already normal."""
    if t[0] == "var":
        return None
    if t[0] == "lam":
        b = step_applicative(t[1])
        return None if b is None else ("lam", b)
    f, a = t[1], t[2]
    f2 = step_applicative(f)
    if f2 is not None:
        return ("app", f2, a)
    a2 = step_applicative(a)
    if a2 is not None:
        return ("app", f, a2)
    return beta(f[1], a) if f[0] == "lam" else None

def normalize(t, fuel=10_000, step=step_normal):
    """Reduce until normal; return (normal form or None, steps used)."""
    for n in range(fuel):
        nxt = step(t)
        if nxt is None:
            return t, n
        t = nxt
    return None, fuel

def church(n):
    """The Church numeral λf. λx. f^n x."""
    body = ("var", 0)
    for _ in range(n):
        body = ("app", ("var", 1), body)
    return ("lam", ("lam", body))

def decode_church(t):
    """Return n if ``t`` is exactly the Church numeral n, else None."""
    if t is None or t[0] != "lam" or t[1][0] != "lam":
        return None
    body, n = t[1][1], 0
    while body[0] == "app" and body[1] == ("var", 1):
        body, n = body[2], n + 1
    return n if body == ("var", 0) else None

def standard_defs():
    """Church encodings used in the lesson, as parsed closed terms."""
    d = {"ZERO": church(0), "ONE": church(1)}
    for name, src in [
        ("TRUE", r"\x. \y. x"), ("FALSE", r"\x. \y. y"),
        ("AND", r"\p. \q. p q p"), ("OR", r"\p. \q. p p q"),
        ("NOT", r"\p. p FALSE TRUE"),
        ("PAIR", r"\a. \b. \f. f a b"),
        ("FST", r"\p. p TRUE"), ("SND", r"\p. p FALSE"),
        ("SUCC", r"\n. \f. \x. f (n f x)"),
        ("PLUS", r"\m. \n. \f. \x. m f (n f x)"),
        ("TIMES", r"\m. \n. \f. m (n f)"),
        ("EXP", r"\m. \n. n m"),
        ("ISZERO", r"\n. n (\z. FALSE) TRUE"),
        ("PRED", r"\n. FST (n (\p. PAIR (SND p) (SUCC (SND p))) "
                 r"(PAIR ZERO ZERO))"),
        ("Y", r"\f. (\x. f (x x)) (\x. f (x x))"),
        ("FACT", r"Y (\r. \n. ISZERO n ONE (TIMES n (r (PRED n))))"),
        ("OMEGA", r"(\x. x x) (\x. x x)"),
        ("I", r"\x. x"), ("K", r"\x. \y. x"),
    ]:
        d[name] = parse(src, d)
    for n in range(10):
        d[str(n)] = church(n)
    return d

def run(src, defs, step=step_normal, fuel=10_000):
    """Parse ``src`` with ``defs`` and normalize; return (term, steps)."""
    return normalize(parse(src, defs), fuel, step)

def main():
    d = standard_defs()
    print("Normal-order reduction of Church encodings (exact beta-steps)")
    for src in ["PLUS 2 3", "TIMES 2 3", "EXP 2 3", "PRED 3", "FACT 3"]:
        nf, steps = run(src, d)
        print(f"  {src:<10} ->  Church numeral {decode_church(nf)}"
              f"   ({steps} steps)")
    print()
    print("Booleans: AND p q = p q p, OR p q = p p q")
    names = {d["TRUE"]: "TRUE", d["FALSE"]: "FALSE"}
    for p in ("TRUE", "FALSE"):
        for q in ("TRUE", "FALSE"):
            a, _ = run(f"AND {p} {q}", d)
            o, _ = run(f"OR {p} {q}", d)
            print(f"  p = {p:<5}  q = {q:<5}  AND -> {names[a]:<5}"
                  f"  OR -> {names[o]}")
    print()
    omega = d["OMEGA"]
    nf, steps = normalize(omega, 1000)
    print("Omega = (λx. x x)(λx. x x)")
    print(f"  one step turns Omega into Omega again: {step_normal(omega) == omega}")
    print(f"  normal form within {steps} steps: {nf is not None}")
    nf, steps = run("K I OMEGA", d)
    print("K I Omega")
    print(f"  normal order:      normal form I after {steps} steps:"
          f" {nf == d['I']}")
    nf, steps = run("K I OMEGA", d, step=step_applicative, fuel=1000)
    print(f"  applicative order: normal form within {steps} steps:"
          f" {nf is not None}")
    print()
    print("Epistemic status: exact computations on these particular terms, "
          "checked\nby an interpreter. They illustrate, but do not prove, "
          "that Church\nnumerals compute arithmetic in general, that Omega "
          "has no normal form\n(proved in the lesson and in Lean) or that "
          "normal order always finds a\nnormal form when one exists (the "
          "standardisation theorem).")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in