Defining a Contribution Kind
On this page
The contribution kinds are not a fixed enum. A kind is a compile-time function from a
(:provides ...) clause to a contribution value, registered under a keyword. The
kernel defines its entire vocabulary this way, with the same macro you would use to add
one. There is no privileged set of built-in kinds and a separate plugin path for the
rest; :tool and a kind you write yesterday are registered identically.
The macro
(defcontribution-kind kind (extension-id form) &body body)
The body is a quasiquote returning the form that constructs your contribution;
defcontribution-kind registers it as the form-compiler for kind. When defextension
parses a (:provides ...) block it dispatches on each clause head to the matching
compiler. A clause whose head has no registered compiler signals
unknown-contribution-kind, so a kind exists exactly when its compiler is registered.
The compiler runs at macroexpansion of defextension, not at install. Its job is
narrow: read the clause syntax, return code that builds one contribution object. The
installing and retracting happen later, through methods on that object.
The shape
A kind is three pieces: a contribution class, an install/retract method pair
specialized on it, and the defcontribution-kind compiler that builds it from a
clause. A synthetic :banner kind that registers a startup line:
(defclass banner-contribution (kli/ext:contribution)
((text :initarg :text :reader banner-text)))
(defmethod kli/ext:install-contribution
((protocol kli/ext:extension-protocol) (c banner-contribution) context)
(declare (ignore context))
(register-banner protocol (banner-text c))
(push c (kli/ext:protocol-installed-contributions protocol))
c)
(defmethod kli/ext:retract-contribution
((protocol kli/ext:extension-protocol) (c banner-contribution) context)
(declare (ignore context))
(unregister-banner protocol (banner-text c))
(setf (kli/ext:protocol-installed-contributions protocol)
(remove c (kli/ext:protocol-installed-contributions protocol)))
c)
(kli/ext:defcontribution-kind :banner (extension-id form)
(destructuring-bind (_ text) form
(declare (ignore _))
`(make-instance 'banner-contribution
:kind :banner :text ,text :source ',extension-id)))
Once those three forms load, a (banner ...) clause compiles inside any extension:
(defextension welcome
(:provides
(banner "kli ready.")))
The clause head is matched by name, so banner works unqualified in the author
package; the helpers it expands into (register-banner, the kli/ext symbols) are
ordinary functions and stay package-qualified.
The kernel does exactly this
:theme is the same five-part shape, in tree:
(defcontribution-kind :theme (extension-id form)
(destructuring-bind (_ name theme-form) form
(declare (ignore _))
`(make-theme-contribution
:name ',(normalize-extension-id name)
:theme ,theme-form
:source ',extension-id)))
The :theme kind ships with its own theme-contribution class, a constructor, and the
install/retract pair beside it. Nothing about it is special: it is a self-contained
domain kind that a subsystem registers when it loads.
The core kinds the protocol appears to "know" are the same. They are
defcontribution-kind forms too: :effect, :method, :tool, :capability,
:live-object, :contract, and :grant. :method compiles to a
make-method-contribution that carries a generic-function name, qualifiers,
specializers, and body; its retractor is remove-method. :tool compiles to a
make-tool wrapped in a contribution. The kernel reaches for the macro you do.
The set stays open across the tree. Each subsystem registers its own kinds at load:
:command, :keybinding, :event-type and :event-handler, and the terminal-UI kinds
:message-renderer, :status-slot, and :widget. A kind lives wherever its domain
lives, not in a central registry of permitted types.
The reversibility contract
A kind is real only when its install-contribution and retract-contribution are
symmetric on (extension-protocol, your-contribution). The :theme pair is the
minimal correct form: install registers the theme and pushes the contribution onto
protocol-installed-contributions; retract unregisters by name and removes the
contribution by identity. That symmetry is not decoration. Deactivation walks the
contributions an extension recorded at install and calls retract-contribution on each
(see Lisp extension anatomy), so a kind whose
retract does not undo its install leaks on every /disable and /reload.
Three rules keep a custom kind honest:
- Register and unregister the same name. Whatever install files under a key, retract removes under that key.
- Record the contribution, drop it by identity. Push on install,
removethe same object on retract, so deactivation has an exact list rather than a guess. - Keep state with the contribution or in protocol storage, never in a global. A
slot on the contribution (
banner-textabove) or(kli/ext:ensure-protocol-storage protocol KEY constructor)dies with the protocol; adefvaroutlives retraction and leaks across reloads.
The :effect kind threads its state through contribution-state because it has no
class of its own; a kind with a class keeps state in slots, as :banner does.
Related
- Contribution kinds — the kinds you reach for before defining one of your own.
- Lisp extension anatomy — install as a recorded transaction, and why every kind retracts.
- Extensions all the way down — why the kind vocabulary being contributed the same way is the whole point.
- The live image — why a running kli can register a new kind and start compiling clauses against it without a restart.