Liking cljdoc? Tell your friends :D

Form-shape

Form-shape is the middle layer in meme's three-layer formatter architecture:

LayerOwnsModule
NotationCall-syntax rendering, delimiter placement, mode (meme vs clj)meme-lang.printer
Form-shapeDecomposing special forms into named semantic slotsmeme-lang.form-shape
StyleOpinions on layout per slot namememe-lang.formatter.canon/style and any user/lang-provided alternative

Form-shape is the language-level semantic vocabulary: it answers what the parts of a form mean, independent of how they're laid out. A lang owns its registry (decision: lang sovereignty); formatters consume it.

Slot vocabulary

A form-shape decomposer returns a vector of [slot-name value] pairs in source order. The following slot names are the stable contract:

SlotMeaningExample
:nameIdentifier being defined or named locallyfoo in (defn foo ...), e in (catch E e ...)
:docDocstring"adds one" in (defn f "adds one" ...)
:paramsParameter vector[x y] in (defn f [x y] ...)
:dispatch-valMultimethod dispatch value, catch class:circle in (defmethod area :circle ...); Exception in (catch Exception e ...)
:dispatch-fnDispatch function= in (condp = x ...); the fn in (defmulti name dispatch-fn)
:testConditional expression(> x 0) in (if (> x 0) ...)
:exprTarget expression for case/condp/threadingx in (case x ...); coll in (-> coll ...)
:bindings[k v ...] binding vector[x 1 y 2] in (let [x 1 y 2] ...)
:as-nameas-> binding namey in (as-> x y ...)
:clauseTest/value pair; value is a [test value] 2-tuple[:circle "c"] in (case x :circle "c" ...)
:defaultCase/condp default branch"other" in (case x 1 "one" "other")
:arityComplete single-arity form ([params] body+)([x] ...) in multi-arity defn
:bodyOrdinary body expression(+ x 1) in (defn f [x] (+ x 1))

A single form generally emits a mix of these — e.g.

(defn greet "says hi" [name] (println name))
;; decomposes to:
;; [[:name greet] [:doc "says hi"] [:params [name]] [:body (println name)]]

Using form-shape

Querying a decomposition

(require '[meme-lang.form-shape :as fs])

(fs/decompose fs/registry 'defn '[foo [x] (+ x 1)])
;=> [[:name foo] [:params [x]] [:body (+ x 1)]]

(fs/decompose fs/registry 'my-unregistered-fn '[1 2 3])
;=> nil   ; no shape — formatters fall back to plain-call rendering

Adding a user macro to the registry

;; Reuse an existing decomposer — my-defn behaves like defn:
(def my-registry
  (assoc fs/registry 'my-defn (get fs/registry 'defn)))

(fs/decompose my-registry 'my-defn '[foo [x] (+ x 1)])
;=> [[:name foo] [:params [x]] [:body (+ x 1)]]

Opt-in structural fallback

When a registry is wrapped with with-structural-fallback, unregistered heads with a recognizable shape are inferred automatically:

  • (HEAD name [params] body*) → defn-like decomposition
  • (HEAD [bindings] body*) → let-like decomposition
(def reg (fs/with-structural-fallback fs/registry))

(fs/decompose reg 'my-defn '[foo [x] (+ x 1)])
;=> [[:name foo] [:params [x]] [:body (+ x 1)]]

(fs/decompose reg 'my-let '[[x 1] (+ x 1)])
;=> [[:bindings [x 1]] [:body (+ x 1)]]

;; No shape match — plain call, no inference
(fs/decompose reg 'my-fn '[1 2 3])
;=> nil

Only these two patterns are inferred because they're unambiguous — narrower rules would misfire on ordinary function calls. Register explicitly for other shapes.

Passing a registry to the formatter

(require '[meme-lang.formatter.canon :as canon])

(canon/format-form '(my-defn foo [x] body)
                   {:width 20
                    :form-shape (fs/with-structural-fallback fs/registry)})
;=> "my-defn( foo [x]
;     body
;   )"

The style map

A style opines on layout over slot names, not form names. The canonical style (meme-lang.formatter.canon/style) is minimal:

{:head-line-slots
 #{:name :doc :params :dispatch-val :dispatch-fn :test :expr :bindings :as-name}

 :force-open-space-for
 #{:name}}

Keys:

  • :head-line-slots — slot names that stay on the head line with the call head when the form breaks. Other slots go into the indented body.
  • :force-open-space-for — slot names whose presence triggers head( (space after open paren) even on flat output. For meme, this is the classic "defn( becomes defn(" rule; any form carrying a :name slot gets the treatment.
  • :slot-renderers (optional) — a map {slot-name → (fn [value ctx] → Doc)} that overrides printer defaults. Useful when a project wants a slot rendered differently, or a new custom slot needs display logic.

Slot renderers and defaults

The printer ships defaults for structural slots whose values aren't plain forms:

SlotDefault renderer behavior
:bindingsColumnar [k v\n k v] layout via binding-vector-doc
:clause[test value] rendered as test value joined by a space

Overrides compose over defaults via map merge — a style may replace one renderer without affecting the others. See meme-lang.printer/default-slot-renderers for the built-ins.

Built-in decomposers

The default meme-lang.form-shape/registry registers these Clojure forms:

FamilyMembers
def*def, def-, defonce, ns, defprotocol
defn*defn, defn-, defmacro
defmulti / defmethodeach its own shape
defrecord / deftypeshared shape
deftest / testingeach its own shape
case / cond / condppair-body shapes
catchdispatch class + binding name
Threading->, ->>, some->, some->>, cond->, cond->>
as->expr + as-name + body
let familylet, loop, for, doseq, binding, with-open, with-local-vars, with-redefs, if-let, when-let, if-some, when-some
if familyif, if-not, when, when-not

Project-local configuration (.meme-format.edn)

meme format discovers a .meme-format.edn file by walking up from the current working directory. If present, its settings become defaults under CLI flags.

Schema:

{:width                 80
 :structural-fallback?  true
 :form-shape            {my-defn defn
                         my-let  let
                         deftask defn}
 :style                 {:head-line-slots #{:name :params :bindings}}}
KeyMeaning
:widthTarget line width (positive integer).
:structural-fallback?Enable shape inference for unregistered heads that look like defn or let.
:form-shapeMap {user-sym → built-in-sym}. Each entry aliases a user macro to an existing registry entry. The target must be a registered head (e.g. defn, let, defmethod).
:stylePartial override of canon/style, merged on top of the defaults. Supports :head-line-slots and :force-open-space-for. :slot-renderers isn't supported from EDN (renderers are functions).

Unknown keys are ignored with a warning so configs remain forward-compatible.

Example — teach the formatter about a project DSL:

;; project-root/.meme-format.edn
{:width 100
 :structural-fallback? true
 :form-shape {my-defn     defn
              defendpoint defn
              do-tx       let}}

After this, my-defn/defendpoint render with defn-like layout, do-tx renders with let-like layout, and any other user macro that looks structurally like defn or let (thanks to :structural-fallback?) also gets layout for free.

Future consumers

Form-shape is designed to serve tools beyond the canonical formatter. Some directions not yet built:

  • LSP semantic tokens — color :name as definition, :params as parameter, :test as keyword-expression distinctly from :body.
  • Lint rules — "docstring should be on its own line", "too many body forms for this operator", "missing :default in case", all phrased against slot structure rather than AST walks.
  • Refactoring operations — "extract arity" reorders :arity slots; "convert single-arity to multi-arity" rewrites slot vector.
  • Doc generators — extract :doc slots from def* forms.

The slot vocabulary is the shared contract these would sit on. Keeping it stable is the reason it's a first-class API rather than a private implementation detail.

See also

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close