Skip to content

Repository files navigation

Aviary

A Jupyter and a Caderno kernel for combinatory logic (combinator calculus): S/K/I, BCKW, and the Smullyan aviary (Bluebird, Cardinal, Mockingbird, Phoenix, Psi, Vireo, the once- and twice-removed permuting birds, and more), evaluated under normal-order (leftmost-outermost, lazy) reduction to full normal form.

K ▲ (M M)
▲        [1 step]

M M (the classic non-terminating self-application) is never touched -- that's normal order's laziness in action. See SPEC.md for the full design and demo.ipynb for a guided tour, including a computational proof-sketch of Smullyan's Mockingbird fondness theorem.

Jupyter/Python Kernel

Install

Requires Python >= 3.10.

pip install aviary-kernel
python -m aviary_kernel.install --user   # or --prefix /path/to/env

Or from a checkout, for development:

python3 -m venv .venv && source .venv/bin/activate
pip install -e .
python -m aviary_kernel.install --prefix .venv

This registers a kernelspec named aviary, displayed as "Aviary (Combinator Calculus)" in Jupyter frontends. Then:

jupyter notebook demo.ipynb        # open the demo
jupyter console --kernel aviary    # or drop into a REPL

(--user installs into your per-user Jupyter kernel directory; --prefix .venv installs into the active virtualenv instead, which is handy for local development without touching your global Jupyter config.)

Usage

A cell is a sequence of lines; each non-empty line is one statement: a magic (%name ...), a definition (Name := expr or Name v1 v2 -> expr), or an expression. # starts a comment.

Expressions

Application is left-associative and implicit -- S K K means ((S K) K). Parentheses group; S K (K I) differs from S (K (K I)). Any built-in bird, or arbitrary Unicode atom (, 🟢, foo_bar), can appear free:

B ▲ 🟢 (K ◆ ●)
▲ (🟢 ◆)        [2 steps]

Names like Q1/Q₁, W'/W′/, and E^/Ê are the same bird under Unicode normalization (subscript digits, primes, superscript one) and explicit ASCII aliases; %ascii on/off picks which script output uses.

Definitions

🟢 x y z -> x (z y)      # rule (a new combinator of arity 3)
Theta := U U             # alias (already preloaded, for reference)

Rules can be recursive (the name may appear in its own body -- that's how you'd hand-roll a sage bird); recursive definitions reduce fine but can't be basis-expanded to a finite S/K/I term (%ski raises an error explaining why, and suggests routing through Y/Θ instead). Redefining a built-in is an error; redefining a user definition replaces it. %undef Name removes a user definition; %defs lists them all.

Magics

Magic Effect
%trace expr step-by-step reduction trace
%whnf expr reduce to weak head normal form only
%ski expr / %sk expr basis-expand to S/K/I (or strict S/K); does not reduce
%fuel N set the step budget for the session (bare form prints it)
%size N set the term-size guard (bare form prints it)
%birds table of the whole registry
%whatis X one bird/definition's name(s), arity, rule, λ-form, and S/K/I form
%defs list user definitions
%undef X remove a user definition
%ascii on|off toggle Q1 vs Q₁-style output

Reduction is fuel-bounded (10 000 steps and 100 000 atoms by default); running out is a warning, not an error -- you get the partial term back and can raise the budget with %fuel/%size and try again:

M M
M M      [10000 steps]
⚠ no normal form after 10000 steps (fuel exhausted); term size 3

Examples

Bracket abstraction is derived, not hand-copied. Every non-recursive bird's S/K/I form comes from Curry's bracket-abstraction algorithm applied to its rule, on demand:

%ski Q₁
S (S (K S) (S (K K) (S (K S) K))) (K (S (K (S I)) K))

%trace shows normal order at work, including descent into stuck arguments once the head sticks:

%trace Φ B C K x y
    0  Φ B C K x y
Φ   1  B (C x) (K x) y
B   2  C x (K x y)
K   3  C x x

Package layout

aviary_kernel/
  terms.py        Term model (Atom | App), pretty-printer
  parser.py        lexer + parser -> Term
  birds.py          the bird registry (verified against two independent sources; see below)
  reduce.py          normal-order reducer: iterative, fuel/size/interrupt-checked
  abstraction.py    bracket abstraction -> S/K/I (and S/K)
  magics.py          %-command dispatch
  kernel.py          AviaryKernel(ipykernel.kernelbase.Kernel)
  install.py         `python -m aviary_kernel.install` writes the kernelspec
  kernelspec/         kernel.json
tests/               pytest suite (parser, birds, reduce, abstraction, kernel end-to-end)
demo.ipynb            living documentation / acceptance demo
SPEC.md               the full design spec

Development

pip install -e '.[test]'
pytest                       # full suite
python -m aviary_kernel.install --prefix .venv   # register the kernel into this venv

The bird registry (birds.py) was cross-checked row by row against the Wolfram Data Repository's Combinator Birds dataset (via its own cited primary source, Chris Rathman's chart, fetched through the Wayback Machine) and combinatorylogic.com's table -- see the header of tests/fixtures_birds.py for the two source discrepancies that surfaced during verification (and how they were resolved) and tests/test_birds.py for the resulting behavioral-equivalence oracles.

Urbit / Caderno Kernel

Aviary also ships as desk/, a self-contained Urbit desk: the %aviary /lib/shoe agent is a sibling Hoon port of this same combinator-calculus engine (same language, same semantics -- SPEC.md §§3-8), usable both interactively from a terminal |shoe session and as a caderno kernel. See SPEC-DESK.md for the full desk-level spec.

Magics: %ski, %sk, %fuel, %defs, %undef (SPEC-DESK.md's v1 scope), plus all six originally-deferred-to-v1.1 magics, now implemented: %trace, %whnf, %size, %ascii, %birds, and %whatis -- full magic-level parity with the Python kernel (SPEC.md §9's protocol-level surface -- do_inspect/do_interrupt have no equivalent in the shoe/sole kernel contract this desk builds on, since Gall pokes are synchronous and %whatis already covers do_inspect's job). %trace's output format matches the Python kernel's exactly: each line is the contracted combinator's canonical name (not its Unicode display form) left-padded to 4 columns, the step number right-padded to 4 columns, two spaces, then the term -- e.g. %trace K x1 (M x1) prints

       0  K x1 (M x1)
K      1  x1

Traces obey the session's fuel the same as a plain expression (a fuel/size warning line, never an error) and elide the middle beyond 200 lines when running under the default (unraised) fuel.

%whnf expr (SPEC.md §6.1/§9) reduces only to weak head normal form -- it contracts head-position redexes but never descends into arguments to normalize them, so %whnf B x1 (M x2) prints B x1 (M x2) [whnf, 0 steps] (stuck: B needs 3 args, has 2) where the plain expression B x1 (M x2) would print B x1 (x2 x2) [1 step] (arguments get normalized once the head is stuck). Matching the Python reference exactly, %whnf's status is always whnf (never normal, even at 0 steps) and it prints only the one result line -- no separate "⚠" warning line on fuel/size exhaustion, unlike a plain expression or %trace.

%size N (SPEC.md §9) is %fuel's sibling: it gets/sets the session's max_size term-size ceiling (previously a fixed 100,000-atom constant, now per-session state, same as fuel).

%ascii on|off (SPEC.md §9) toggles the session's display script for bird names whose canonical name differs from their preferred display form -- e.g. Q1 prints as Q₁ by default (%ascii off) and as Q1 under %ascii on.

%birds (SPEC.md §9) prints the whole registry, one fixed-width row per bird, sorted by canonical name -- unlike the Python reference (which also emits a text/markdown table for rich frontends), this is plain text only, since the shoe/sole kernel contract has no rich-display channel; it's the same information, just the one rendering the contract can actually carry.

%whatis X (SPEC.md §9) prints a four-line card for a bird (by canonical name or alias) or a user/built-in definition (including Θ, which is preloaded exactly as if Θ := U U had been typed, per SPEC.md §5.2): the name(s)/kind/arity header, the rule, the λ-form, and the S/K/I expansion (or an (unexpandable: ...) fallback for a recursive user rule, same wording %ski itself would error with).

Tab completion: app/aviary.hoon implements +tab-list for real (every bird name and alias, every magic name, and the session's own user definitions, each with a one-line detail) -- this is Aviary's half of SPEC-DESK.md §1/§10's deferred "completion/tab-list beyond shoe's default," reusing the vendored lib/language-server-complete.hoon prefix-matching machinery /lib/shoe's own +tab arm already calls for any real sole session (terminal |shoe, or |dojo/linked to one). Whether a caderno notebook cell ever exercises this is a separate, Caderno-side question -- see caderno#16.

The desk is distributed from ~magbel. On any ship:

|install ~magbel %aviary

(To run it from a local checkout instead — for development, or to install from your own ship — see the development loop below.)

Then in caderno: create a notebook, choose kernel "aviary". Kernel discovery needs the /x/sole/sessions scry (caderno's README notes this requirement); the vendored lib/shoe.hoon provides it, so no extra setup is needed on aviary's side.

Desk layout

desk/
  sys.kelvin              [%zuse 408]
  desk.bill                :~  %aviary  ==
  app/aviary.hoon           shoe agent (thin transport shim over lib/aviary)
  lib/aviary.hoon            the pure engine: lexer, parser, registry, reducer,
                             bracket abstraction, magics, pretty-printer
  sur/aviary.hoon            term/bird/def/env types
  tests/lib/aviary.hoon      -test-thread-style regression tests
  lib/shoe.hoon              VENDORED from north master (%eval-command + the
                             /x/sole/sessions scry live here)
  lib/sole.hoon, lib/default-agent.hoon, lib/dbug.hoon, lib/skeleton.hoon,
  lib/test.hoon, sur/sole.hoon, mar/eval-command.hoon,
  mar/bill.hoon, mar/sole/action.hoon, mar/sole/effect.hoon,
  lib/language-server-complete.hoon, lib/language-server-parser.hoon
                             VENDORED, byte-for-byte, from north master /
                             base master (see SPEC-DESK.md §1-2 for exactly
                             which and why; the last five surfaced only when
                             actually installing on a live ship -- desk.bill
                             needs mar/bill.hoon to validate, Eyre needs
                             mar/sole/{action,effect}.hoon to JSON-serialize
                             sole-effect facts for any HTTP/channel client,
                             and lib/shoe.hoon's tab-completion needs the
                             two flat-named language-server-*.hoon files)
tests/
  test-engine.sh             headless engine tests via `urbit eval` (SPEC-
                             DESK.md §8); needs $URBIT_BIN or ~/bin/urbit
  test-engine-cases.hoon     the actual §8 test battery (one Hoon expression)
tests/test_hoon_registry.py  Python <-> Hoon registry cross-test (SPEC-
                             DESK.md §7): fails hard on any drift

Run the Hoon engine tests with make test-hoon (or bash tests/test-engine.sh directly); $URBIT_BIN overrides the urbit binary used (defaults to ~/bin/urbit, falling back to urbit on PATH). Run everything, Python and Hoon, with make test-all.

Development loop

|mount %aviary
:: rsync/copy desk/ into the mounted pier directory, e.g.:
rsync -a desk/ /path/to/pier/aviary/
|commit %aviary

Then either talk to it directly from dojo (|shoe %aviary-style interactive session, once mounted as a running app) or drive it from caderno as above.

About

A Jupyter kernel for combinator calculi.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages