4 items · 1 systems

2026-08-08 Sat

Learning, Lisp & Q&A

4

  1. 01

    Lesson: build/orrery.el — require, provide, and finding your own root

    [2026-08-08] · src build/orrery.el

    • lesson
    • elisp

    build/orrery.el is the entry point: five require calls wire the other five files together (orrery-model, orrery-feed, orrery-ir, orrery-render-org, orrery-render-html), and orrery-build chains model+feed to ir to two renderers to dist/. Every stage in the pipeline is named exactly once, in under forty lines.

    The Lisp idea: require/provide is Elisp's entire module system, and it is smaller than it looks. (provide 'orrery-ir) at the bottom of orrery-ir.el does one thing — pushes the symbol orrery-ir onto the global features list. (require 'orrery-ir) at the top of orrery.el checks whether that symbol is already in features; if not, it searches load-path for a file named orrery-ir.el (or .elc) and loads it, which runs its own provide and satisfies the check. There is no namespacing and no export list — every defun becomes globally visible the moment its file loads, which is exactly why the orrery-ir-- double-dash-means-internal convention in this repo's CLAUDE.md is the only thing marking a function private. Elisp itself does not enforce it.

    orrery-root (orrery.el:23-29) is the other half of "finding yourself": it derives the repo root from load-file-name — the path Emacs is loading this very file from — instead of a hardcoded path or an env var. That is what lets (require 'orrery) produce a correct orrery-root identically from a git checkout, a nix store path, or a CI runner's checkout, with zero configuration.

    Why

    Notice orrery-build-ir (orrery.el:38) writes nothing and returns a value, while orrery-build (orrery.el:48) is the only function that touches disk. That split is what a later lesson on determinism will unpack in full — it is what lets make golden build an IR from test fixtures and diff it with no dist/ involved at all. For now, just notice the seam exists before reading past it.

    Exercise (verifiable): from the repo root, run:

    emacs -Q --batch -L build --eval "(progn (require 'orrery) (princ orrery-root))"

    Confirm it prints your checkout's absolute path, ending in a slash. Then copy the checkout to a different directory and run the same command again with no edit to orrery.el — the root changes to match. That is load-file-name doing the work the docstring describes.

    Review scheduled: first review of build-shape due 2026-08-09. No reviews were due this run — data/state/srs.json did not exist before it, so there is nothing yet to surface.

  2. 02

    cl-lib vs the built-in seq functions

    [2026-08-08] · src build/orrery-ir.el, build/orrery-render-html.el, build/orrery-feed.el

    • ask
    • elisp

    None of the six files under build/ require cl-lib. Every sequence operation goes through seq.el instead: seq-take (orrery-ir.el:86), seq-filter (orrery-ir.el:111), seq-doseq (orrery-ir.el:142,154), seq-map-indexed (orrery-render-html.el:210), seq-empty-p (orrery-render-org.el:54). seq.el's functions are generic over sequences — the same seq-take works on a list or a vector, dispatching internally — where the cl-lib equivalents (cl-subseq, cl-remove-if) mostly assume lists and exist as a Common Lisp compatibility shim.

    Why

    This codebase's JSON-shaping layer deliberately mixes lists and vectors (see the first item above), so a sequence function that only worked on one shape would force a type check at every call site. seq's genericity is what lets orrery-ir.el pass a list here and a vector there without caring. Reach for cl-lib only for what seq doesn't cover — cl-defstruct, cl-case, &key arguments — and reach for pcase-let (orrery-feed.el:115, destructuring (panel . items) off a cons) before cl-lib's destructuring-bind. On sequences specifically, seq wins by default; this repo's own zero cl-lib requires is the evidence.

  3. 03

    Plists vs alists: this repo's split, and why it's the right one

    [2026-08-08] · src build/orrery-model.el

    • ask
    • elisp

    Both are just lists of conses; the difference is what you do with the keys. A plist ((:id "x" :weight 3)) suits a small fixed vocabulary you know at write-time — plist-get/plist-put compare keys with eq, and keyword symbols are interned, so that's a pointer check. An alist (((key . val) ...)) suits keys you don't control — assoc compares with equal by default, so arbitrary strings work, and duplicate keys shadow instead of erroring.

    Why

    orrery-model--node-properties (orrery-model.el:47-58) builds alists exactly where that matters: an Org PROPERTIES drawer has keys Greg typed by hand, and orrery-model--prop! looks them up with (assoc (upcase key) props) (orrery-model.el:64,72). Everywhere else — panel plists, item plists, the IR itself — the key set is fixed and known, so it's plists via plist-get. The split isn't incidental either: as the JSON item above shows, json-serialize wants plists for objects, so keeping downstream data as plists means orrery-ir.el hands it over with zero conversion. Alist at the boundary where keys are foreign text, plist everywhere the keys are your own vocabulary — this repo holds that line consistently.

  4. 04

    Why orrery-ir.el uses vectors, not lists, for JSON arrays

    [2026-08-08] · src build/orrery-ir.el

    • ask
    • elisp

    Emacs's native json-serialize treats any Lisp list you hand it as a JSON object, never an array: nil is ambiguous between "false", "null" and "empty object", and a non-empty list gets read as alist/plist key-value pairs. A vector is the one shape json-serialize always turns into a JSON array regardless of contents. That's orrery-ir--strings (orrery-ir.el:51-55): it wraps (or list []) in vconcat purely to flip list to vector, because an item's :tags is a real Lisp list of strings, and handed over unchanged it would be misread as an object.

    Why

    orrery-ir--object (orrery-ir.el:40) builds its output as a flat plist and hands that to json-serialize too — and that one is meant to become an object. Same underlying Lisp type, two different JSON shapes, decided entirely by which function built the list and whether it got vconcat'd. Miss that on a new list-valued field and you get a corrupted digest, not an error.