Macros#
Kvist macros run before ordinary parsing and Odin emission. They transform Kvist forms into Kvist forms. They do not run at program runtime, and they do not introduce a dynamic runtime object model.
Use macros when the shape of the source is the important part:
- declaring several related forms from one compact declaration
- validating a small DSL before normal lowering
- generating repetitive, predictable Odin-shaped code
- adapting field selectors such as
.nameinto specialized helpers - reading small compile-time resources into generated declarations
Prefer ordinary functions when runtime values are enough.
Basic Form#
(defmacro unless [condition & body]
`(if ~condition
(do)
(do ~@body)))
Macro parameters receive source forms. A rest parameter is written as & name
at the end of the parameter vector and receives zero or more forms.
Use defmacro- for package-private macros.
Macros see source keywords such as :else and :db/add as source forms. That
is separate from ordinary runtime keyword values in Kvist code. Macro-time
keyword?, keyword, name, and source work on the source
representation; after expansion, ordinary lowering turns keyword literals in
emitted code into runtime keyword values.
For a small runnable version of this shape, see examples/language/macros.kvist.
Quoting#
'form is reader syntax for (quote form). In the macro evaluator it returns
one source form without evaluating it. In ordinary runtime code it produces a
first-class immutable Data value. Data and macro syntax are deliberately
separate: syntax retains compiler context and source locations, while runtime
data does not.
Backtick is reader syntax for quasiquote. It builds a form while allowing
selected parts to be evaluated:
`(defn ~fn-name [] -> int
~value)
~ is reader syntax for unquote and inserts one evaluated macro value.
~@ is reader syntax for splice and inserts zero or more forms into a
quasiquoted list, vector, or brace literal.
`(do ~@body)
The long forms quote, quasiquote, unquote, and splice remain valid.
The reader syntax is the preferred spelling for macro code.
Kvist backtick is simple quasiquote sugar; it does not auto-qualify symbols or
create Clojure-style auto-gensyms.
Returning Forms#
Most expression macros return one form. Top-level DSL macros often return
multiple forms with forms:
(defmacro defentity [name fields]
(let [make-name (symbol (str "make-" (name name)))]
(forms
`(defstruct ~name ~fields)
`(defn ~make-name [] -> ~name
(~name {})))))
concat also returns a sequence of forms by concatenating evaluated form
sequences.
Form Inspection#
The macro evaluator provides predicates for source shapes:
(form? x)
(list? x)
(vector? x)
(brace? x)
(set? x)
(symbol? x)
(keyword? x)
(string? x)
(number? x)
keyword? here means "is this source form spelled like :name?", not "does
this runtime expression have type keyword?".
More specific literal classifiers, such as integer, float, boolean, or nil
checks, should be local macro helpers over source, number?, symbol?, and
ordinary string predicates.
Field selector predicates should use the same local-helper style:
(defmacro- field-selector? [form]
(and (symbol? form)
(> (count (source form)) 1)
(= (slice (source form) 0 1) ".")))
Field-place decomposition should also be local macro source. For example,
assoc-style macros can split user.profile.name into target user and
selector .profile.name with private helpers over source, count, slice,
and symbol; this is ordinary source code, not evaluator knowledge.
Sequence helpers for form collections:
(first xs)
(rest xs)
(nth xs i)
(count xs)
(contains? xs value)
(slice xs start)
(slice xs start end)
(concat xs ys ...)
(not value)
(and a b ...)
(or a b ...)
(parse-int text) ;; int or nil
Small form-sequence transforms can be written as ordinary recursive macros when macro code needs to inspect or rewrite source lists:
(map source forms)
(filter symbol? forms)
(some? keyword? forms)
(every? symbol? forms)
These are iterative macro-time operations over source forms, not runtime
kvist:arr helpers. Their unary operation may be a supported builtin such as
symbol?, keyword?, source, name, or text, or a unary user macro.
some? and every? short-circuit. Prefer these helpers to recursive
first/rest walkers: large generated forms then use bounded native stack.
Constructors and text helpers:
(list a b c)
(vector a b c)
(brace key value)
(symbol "make-Point")
(keyword "else")
(name .field) ;; "field"
(name :else) ;; "else"
(name pkg.member) ;; "member" for rewritten source-package symbols
(text form-or-value)
(source :db/add) ;; ":db/add"
(str "prefix-" (name sym))
(gensym "tmp")
(subst template names values)
Use source when the original token spelling is data, such as EDN-style
keywords in DSLs. name and text normalize symbols and keywords. When a
macro receives a source form whose package-qualified symbol was rewritten for
emission, name reports the original member name rather than the generated
implementation symbol, so package DSLs do not need generated-name checks.
parse-int returns an integer on success and nil on failure, so 0
remains a valid truthy parsed result in macro conditionals.
Use subst for template-style source replacement. names and values
are source-form lists of the same length.
Use keyword when a macro needs to emit a keyword literal back into
ordinary Kvist code:
(defmacro else-branch []
(keyword "else"))
Macro-time helpers intentionally cover only source forms and simple scalar values. If a helper operates on runtime arrays, maps, sets, or owned strings, use the ordinary package helper in runtime Kvist code instead.
Use error for macro validation failures:
(if (field-selector? field)
field
(error "expected a field selector such as .name"))
Errors raised while expanding macros include the macro expansion context.
Compile-Time Files#
kvist.read-file is available to macros and resolves relative paths against the
source file being compiled:
(defmacro def-template []
(let [text (kvist.read-file "template.html")]
`(def template: string ~text)))
Use this for small source assets or generated constants. Runtime file work
belongs in ordinary Kvist code such as io.read.
Hygiene#
Kvist macros are explicit source rewriting, not a hygienic macro system. Use
gensym for generated locals that must not collide with user code:
(defmacro once [expr]
(let [tmp (gensym "value")]
`(let [~tmp ~expr]
~tmp)))
Package-qualified symbols and generated symbols are emitted exactly as source forms, then go through normal Kvist package and name lowering.
When generating typed declarations, the : belongs to the generated name
symbol:
(let [typed-name (symbol (str (name const-name) ":"))]
`(def ~typed-name string "value"))
This expands to a normal typed declaration such as:
(def Label: string "value")
Inspecting Expansions#
Use the CLI to inspect macro output before Odin lowering:
kvist macroexpand file.kvist '(some-macro arg)'
Use kvist expand to inspect the generated Odin after macro expansion and
ordinary lowering:
kvist expand file.kvist '(some-expression)'
Macro code should produce clear Kvist forms first; readable Odin follows from that.
Examples#
- examples/language/macros.kvist - small expression macro
- examples/language/macro-dsl.kvist - macro that emits several top-level forms
- examples/language/macro-messages.kvist - declaration DSL with generated structs, union entries, and constructors
- kvist-lang/html - external package with form inspection, validation, and generated rendering