Navigation
Copy page

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

lisp
(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:

lisp
(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:

lisp
(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:

lisp
(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, remove the 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-text above) or (kli/ext:ensure-protocol-storage protocol KEY constructor) dies with the protocol; a defvar outlives 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.