def, defn, defn-
Expand to define. A docstring or attribute map before the parameter vector is accepted and dropped, because define has no docstring slot.
Forma is an incubation project within WorldVM.
Skip to contentClojure in Forma · a shipped library · pre-alpha
Eight macros in one file, imported like any other module.
preludes/clojure.forma gives a module def, defn, defn-, if-let, when-let, if-not, when-not, comment, and threading. Every one is an ordinary macro that expands to core Forma, so the program it produces is checked by Hindley–Milner inference and carries Forma's effects and contracts. It is spelling, not semantics: if takes a Bool, absence is Option, and nil is a Unit value.
(: quote-for (-> Int (Option Int) String))
(defn quote-for [subtotal coupon]
(-> (after-coupon subtotal coupon)
(+ (shipping-for subtotal))
(str " cents")))Stage 0 of RFC 0021 · where Clojure habits stop
01 Scope
"Clojure support" could mean four different things. Exactly one of them exists today, and this page is only about that one.
| What it would mean | Status | |
|---|---|---|
| A spelling library | Clojure's familiar heads as macros over Forma semantics | Shipped preludes/clojure.forma, this page |
| A Clojure surface | A language with Clojure's reader, (:key m), #{...}, and its own diagnostics | Proposed RFC 0021 stage 5 |
| A Clojure adapter | Typechecking existing .clj files, with declaration files and a gradual boundary | Proposed RFC 0022 stage 5 |
| A Clojure backend | Emitting .cljc that runs on the JVM and in ClojureScript | Proposed RFC 0021 stage 6 |
| Today | Write Forma, spell it like Clojure, run it on the Forma engine | Nothing on this page runs on a JVM |
The library exists because of one small engine change. Forma used to reject def, defn, defmacro, lambda, and let* in the reader, before any library could claim them. Those spellings are now rejected after expansion, so a macro in scope may define what they mean — and if none is, the old message still arrives at the form you wrote:
report.forma:1:1 error surface/invalid-form
Use define instead of defn02 The library
This is the whole thing. Each macro is a template over core Forma: defn builds a define of a fn, and if-let builds a match on Option. Nothing here is privileged — a prelude you write has the same powers.
; clojure.forma
; -----------------------------------------------------------------------------
; Clojure spellings over Forma semantics (RFC 0021, stage 0).
;
; An ordinary macro library for file modules. It lets code written with
; Clojure habits read as Forma: every macro expands to core Forma, so types,
; diagnostics, and artifacts are Forma's. It does not add Clojure semantics:
; `if` still needs a Bool, `nil` is not `None`, and numbers are Int or Float.
;
; Limits: `defn` takes one parameter vector (no multi-arity bodies) and drops
; a docstring or attribute map, because `define` has no docstring slot.
; -----------------------------------------------------------------------------
(import "./kernel.forma" [-> ->>])
(export def defn defn- if-let when-let if-not when-not comment -> ->>)
;; def — bind a value to a name
(macro (def name value) `(define ~name ~value))
;; defn — define a function from a parameter vector and a body
;;
;; (defn area "Rectangle area." [w h] (* w h))
;;
;; Parts before the parameter vector (a docstring, an attribute map) are
;; accepted and dropped.
(macro (defn name parts ...)
(if (sexpr-vector? (first parts))
`(define ~name (fn ~@parts))
(if (sexpr-vector? (nth parts 1))
`(define ~name (fn ~@(rest parts)))
`(define ~name (fn ~@(rest (rest parts)))))))
;; defn- — module-private in Clojure; Forma modules export by name, so this
;; is `defn`
(macro (defn- name parts ...) `(defn ~name ~@parts))
;; if-let — branch on an Option, binding the value inside Some
;;
;; (if-let [user (find-user id)] (greet user) "nobody")
(macro (if-let binding then else)
(let [items (sexpr-items binding)]
`(match ~(nth items 1) (Some ~(first items)) ~then None ~else)))
;; when-let — run the body with the value inside Some, or return nil
(macro (when-let binding body ...)
(let [items (sexpr-items binding)]
`(match ~(nth items 1) (Some ~(first items)) (do ~@body) None nil)))
;; if-not — inverse if
(macro (if-not test then else) `(if ~test ~else ~then))
;; when-not — run the body when the test is false
(macro (when-not test body ...) `(if ~test nil (do ~@body)))
;; comment — ignore the enclosed forms
(macro (comment forms ...) nil)def, defn, defn-Expand to define. A docstring or attribute map before the parameter vector is accepted and dropped, because define has no docstring slot.
if-let, when-letExpand to match on Option. They bind the value inside Some; they do not test truthiness.
if-not, when-not, commentOrdinary templates. when-not yields nil, which in Forma is a Unit value.
->, ->>Re-exported from kernel.forma, where they already lived. Threading is the one Clojure habit Forma always had.
03 An example
This is a conformance fixture, not a sketch. The engine checks it on every run of pnpm check, and the interface below is its own output.
; orders.forma
; -----------------------------------------------------------------------------
; Clojure spellings over Forma semantics (RFC 0021, stage 0).
;
; Every head below that Clojure programmers recognise — `def`, `defn`, `defn-`,
; `if-let`, `->` — is an ordinary macro imported from preludes/clojure.forma.
; The semantics are Forma's: `if` takes a Bool, absence is `Option`, and the
; annotations are checked by Hindley-Milner inference.
; -----------------------------------------------------------------------------
(import "./clojure.forma" [def defn defn- if-let ->])
(export quote-for receipt)
(def free-shipping-from 5000)
(def flat-shipping 700)
;; `defn-` is module-private in Clojure. Forma modules export by name, so it
;; is `defn` with a comment.
(: shipping-for (-> Int Int))
(defn- shipping-for "Shipping in cents for a subtotal." [subtotal]
(if (>= subtotal free-shipping-from) 0 flat-shipping))
;; `if-let` binds the value inside `Some`. There is no truthiness and no nil
;; punning: the coupon is an `Option Int`, so the missing case is `None`.
(: after-coupon (-> Int (Option Int) Int))
(defn after-coupon [subtotal coupon]
(if-let [off coupon]
(max 0 (- subtotal off))
subtotal))
;; `->` threads the value first, as in Clojure.
;; This is the whole module's job: one receipt line.
(: quote-for (-> Int (Option Int) String))
(defn quote-for [subtotal coupon]
(-> (after-coupon subtotal coupon)
(+ (shipping-for subtotal))
(str " cents")))
(define receipt (quote-for 4200 (Some 500)))(: quote-for (-> Int (Option Int) String))
(: receipt String) ; = "4400 cents"Three things in that output are worth naming.
(: quote-for (-> Int (Option Int) String)) is verified against the body. Drop it and inference still runs, but unannotated arithmetic widens to Float, so the signature you get is (-> Float (Option Float) String).receipt was evaluated while the module was checked. "4400 cents" is a compile-time value in the module's interface, not a test assertion written by hand.Option Int. if-let binds what is inside Some. There is no truthy value and no nil to pun on, which is the whole difference between this library and Clojure.04 Where habits stop
Each block is the compiler's own diagnostic for a fixture in the repository, located at the line and column you wrote — inside the defn body, not inside the macro expansion.
truthiness.forma:7:7 error typecheck/type-mismatch
Cannot unify Option<Int> with Boolnil-punning.forma:7:26 error typecheck/type-mismatch
Type mismatch: Unit vs Intkeyword-access.forma:7:3 error typecheck/type-mismatch
Cannot unify :customer with t1 -{e3}-> t2tags.forma:3:11 error read/syntax
Unexpected character: '#'The first two are the design working: Forma's semantics win, and the error lands on the author's expression. The last two are the design's current edge. (:key m) and #{…} are not spellings a macro can claim — one needs an application-with-a-non-symbol-head rule, the other a reader switch. RFC 0021 calls these interposition points and lexical profiles, and they arrive with a surface, not with a macro library. Use (get m :key) until then.
05 Limits
Everything below is current behavior, not a plan.
defn fails with an arity error raised inside the macro, with no message and no span in your file. It is pinned as a fixture so that fixing it is visible.gapwhen-let wants a Unit bodyIts None branch is nil, so a body that returns a value unifies that value with Unit. (defn f [o] (when-let [v o] v)) infers (-> (Option Unit) Unit) instead of failing.gapdefn accepts a docstring and an attribute map and discards both, because define has no docstring slot.gapdefn- does not make anything privateForma modules export by name, so defn- is defn. A name is private because it is absent from export.by designdefn still reports the retired spelling.by designThe measurement that decides whether this goes further is an agent benchmark: do models writing Forma need fewer repair rounds when their Clojure habits parse? RFC 0021 holds its later stages behind that result.
06 What's next
Each step is a proposal with a gate, and none is scheduled.
| Step | What changes | Where it is written down |
|---|---|---|
| A Clojure surface | A language declaration: a reader profile for #{…} and #(…), (:key m) as an application rule, diagnostics in Clojure's terms, and dialect/unsupported naming each construct it cannot check | RFC 0021 stage 5 |
| A trusted kernel | A small checker that re-checks every elaborated program, so a surface cannot make an ill-typed program pass | RFC 0022 stage 1 |
| A Clojure adapter | Checking existing Clojure source through declaration files, Dynamic, and casts | RFC 0022 stage 5 |
A .cljc backend | Running the surface backwards over checked core terms, plus a kernel port and differential conformance against the TypeScript VM | RFC 0021 stage 6 |
| Today | A macro library, one import away | The library above |
Racket, Scheme, and Common Lisp follow the same route, in that order. The homepage's Lisp targets section has the short version, and RFC 0022's "Lisp targets" has the semantic gaps and the gate for each.