Namespaces are organized in three layers: meme.tools.* (generic), meme-lang.* (language), meme.* (CLI).
The public API for reading and printing meme syntax, organized in three tracks:
text-to-form form-to-text
meme str ──→ meme->forms ──→ forms ──→ forms->meme ──→ meme str
clj str ──→ clj->forms ──→ forms ──→ forms->clj ──→ clj str
text-to-text (compositions)
meme str ──→ meme->clj ──→ clj str
clj str ──→ clj->meme ──→ meme str
(meme-lang.api/meme->forms s)
(meme-lang.api/meme->forms s opts)
Read a meme source string. Returns a vector of Clojure forms. All platforms.
Options:
:resolve-keyword — function that resolves auto-resolve keyword strings ("::foo") to keywords at read time. When absent on JVM/Babashka, :: keywords are deferred to eval time via (read-string "::foo"). Required on CLJS (errors without it, since cljs.reader cannot resolve :: in the correct namespace).:read-cond — :preserve to return ReaderConditional objects instead of evaluating reader conditionals. Default: evaluate #? for the current platform. Use :preserve for lossless clj->meme->clj roundtrips of .cljc files.:resolve-symbol — function that resolves symbols during syntax-quote expansion (e.g., foo → my.ns/foo). On JVM/Babashka, run-string/run-file/start inject a default that matches Clojure's SyntaxQuoteReader (inlined in meme-lang.run). When calling meme->forms directly, symbols in syntax-quote are left unqualified unless this option is provided. On CLJS, no default is available.(meme->forms "+(1 2 3)")
;=> [(+ 1 2 3)]
(meme->forms "def(x 42)\nprintln(x)")
;=> [(def x 42) (println x)]
Note: meme->forms may return internal record types for forms that preserve source notation: MemeRaw (for hex numbers, unicode escapes, etc.) wraps a :value and :raw text; MemeAutoKeyword (for :: keywords) wraps a :raw string. These are unwrapped to plain values by step-expand-syntax-quotes (which run-string/run-file call before eval). If you need plain Clojure values from meme->forms, compose with meme-lang.stages/step-expand-syntax-quotes.
(meme-lang.api/forms->meme forms)
Print a sequence of Clojure forms as meme text. All platforms.
Note: Reference types (atoms, refs, agents) print as #object[...] which cannot be round-tripped. This matches Clojure's own behavior.
(forms->meme ['(+ 1 2 3)])
;=> "+(1 2 3)"
(meme-lang.api/forms->clj forms)
Print Clojure forms as a Clojure source string. All platforms.
(forms->clj ['(def x 42) '(println x)])
;=> "(def x 42)\n\n(println x)"
(meme-lang.api/clj->forms clj-src)
Read a Clojure source string, return a vector of forms. JVM/Babashka only.
(clj->forms "(defn f [x] (+ x 1))")
;=> [(defn f [x] (+ x 1))]
(meme-lang.api/format-meme-forms forms)
(meme-lang.api/format-meme-forms forms opts)
Format Clojure forms as canonical (width-aware, multi-line) meme text. Uses indented parenthesized form for calls that exceed the line width. Preserves comments from :meme-lang/leading-trivia metadata (attached by the pipeline's scan stage). All platforms.
Options:
:width — target line width (default: 80)(format-meme ['(defn greet [name] (println (str "Hello " name)))])
;=> "defn(greet [name]\n println(str(\"Hello \" name)))"
(meme-lang.api/meme->clj meme-src)
(meme-lang.api/meme->clj meme-src opts)
Convert meme source string to Clojure source string. All platforms. Equivalent to (forms->clj (meme->forms meme-src opts)).
Options: same as meme->forms (:resolve-keyword, :read-cond).
(meme->clj "println(\"hello\")")
;=> "(println \"hello\")"
(meme-lang.api/clj->meme clj-src)
Convert a Clojure source string to meme source string. JVM/Babashka only. Equivalent to (forms->meme (clj->forms clj-src)).
Known limitation: Clojure's reader expands reader sugar before meme sees the forms. '(f x) becomes (quote (f x)) → quote(f(x)) instead of 'f(x). Similarly, @atom → clojure.core/deref(atom), and #(+ % 1) → fn*([p1__N#] +(p1__N# 1)). The meme->clj->meme roundtrip preserves semantics but not notation for these forms.
(clj->meme "(defn f [x] (+ x 1))")
;=> "defn(f [x] +(x 1))"
Low-level Doc tree builder. Most callers should use formatter.flat or formatter.canon instead.
(meme-lang.printer/to-doc form mode)
Convert a Clojure form to a Wadler-Lindig Doc tree. mode is :meme (call notation) or :clj (standard Clojure with reader sugar). The Doc tree is passed to meme.tools.render/layout for final string output.
(meme-lang.printer/extract-comments ws)
Extract comment lines from a :meme-lang/leading-trivia metadata string. Returns a vector of trimmed comment strings, or nil.
Flat (single-line) formatter. Composes printer + render at infinite width.
(meme-lang.formatter.flat/format-form form)
Format a single Clojure form as flat meme text (single-line).
(format-form '(+ 1 2))
;=> "+(1 2)"
(format-form '(:balance account))
;=> ":balance(account)"
(format-form '(def x 42))
;=> "def(x 42)"
(meme-lang.formatter.flat/format-forms forms)
Format a sequence of Clojure forms as flat meme text, separated by blank lines.
(meme-lang.formatter.flat/format-clj forms)
Format Clojure forms as Clojure text with reader sugar ('quote, @deref, #'var).
Canonical (width-aware) formatter. Composes printer + render at target width. Used by meme format CLI.
(meme-lang.formatter.canon/format-form form)
(meme-lang.formatter.canon/format-form form opts)
Format a single Clojure form as canonical meme text. Width-aware — uses indented multi-line layout for forms that exceed the target width. Preserves comments from :meme-lang/leading-trivia metadata.
Options:
:width — target line width (default: 80)(meme-lang.formatter.canon/format-forms forms)
(meme-lang.formatter.canon/format-forms forms opts)
Format a sequence of Clojure forms as canonical meme text, separated by blank lines. Preserves comments from :meme-lang/leading-trivia metadata, including trailing comments after the last form.
(meme-lang.repl/input-state s)
(meme-lang.repl/input-state s opts)
Returns the parse state of a meme input string: :complete (parsed successfully), :incomplete (unclosed delimiter — keep reading), or :invalid (malformed, non-recoverable error). Used internally by the REPL for multi-line input handling; also useful for editor integration.
The optional opts map is forwarded to stages/run — useful for callers that need :: keywords or custom parsers to be resolved during input validation.
(input-state "+(1 2)") ;=> :complete
(input-state "f(") ;=> :incomplete
(input-state "(bare parens)") ;=> :invalid
(meme-lang.repl/start)
(meme-lang.repl/start opts)
Start the meme REPL. Reads meme syntax, evaluates as Clojure, prints results.
Options:
:read-line — custom line reader function (default: read-line, required on ClojureScript):eval — custom eval function (default: eval, required on ClojureScript):resolve-keyword — function to resolve :: keywords at read time (default: clojure.core/read-string on JVM; required on CLJS for code that uses :: keywords):prelude — vector of forms to eval before the first user input (e.g., guest language standard library)On JVM/Babashka, :resolve-symbol is automatically injected (matching Clojure's syntax-quote resolution, inlined in meme-lang.run) unless explicitly provided.
$ bb meme repl
meme REPL. Type meme expressions, balanced input to eval. Ctrl-D to exit.
user=> +(1 2)
3
user=> map(inc [1 2 3])
(2 3 4)
The prompt shows the current namespace (e.g., user=> on JVM/Babashka, meme=> on ClojureScript).
Run .meme files or meme source strings.
(meme-lang.run/run-string s)
(meme-lang.run/run-string s eval-fn)
(meme-lang.run/run-string s opts)
Read meme source string, eval each form, return the last result. Strips leading #! shebang lines before parsing. The second argument can be an eval function (backward compatible) or an opts map.
Options (when passing a map):
:eval — eval function (default: eval; required on CLJS):resolve-keyword — function to resolve :: keywords at read time (default: none — :: keywords resolve at eval time in the file's declared namespace. Required on CLJS for code that uses :: keywords):prelude — vector of forms to eval before user code (e.g., guest language standard library)On JVM/Babashka, :resolve-symbol is automatically injected (matching Clojure's syntax-quote resolution, inlined in meme-lang.run) unless explicitly provided.
(run-string "def(x 42)\n+(x 1)")
;=> 43
(meme-lang.run/run-file path)
(meme-lang.run/run-file path eval-fn)
(meme-lang.run/run-file path opts)
Read and eval a .meme file. Returns the last result. Uses slurp internally (JVM/Babashka only). Second argument follows same convention as run-string.
Automatically detects guest languages from file extension via meme.registry/resolve-by-extension. If a registered lang matches the extension, its :run function handles prelude and custom parser.
(run-file "test/examples/tests/01_core_rules.meme")
Explicit pipeline composition. Each stage is a ctx → ctx function operating on a shared context map with keys :source, :opts, :cst, :forms.
(meme-lang.stages/step-parse ctx)
Parse source string into a lossless CST via the unified Pratt parser. Scanning (character dispatch, trivia) and parsing (structure) are handled in a single pass. Uses meme grammar by default, or (:grammar opts) if provided. Reads :source, assocs :cst.
(meme-lang.stages/step-read ctx)
Lower CST to Clojure forms. Reads :cst, :opts, assocs :forms.
(meme-lang.stages/step-expand-syntax-quotes ctx)
Expand syntax-quote AST nodes (MemeSyntaxQuote) into plain Clojure forms (seq/concat/list). Also unwraps MemeRaw values. Only needed before eval — tooling paths work with AST nodes directly.
(meme-lang.stages/run source)
(meme-lang.stages/run source opts)
Run the pipeline: step-parse → step-read. Returns the complete context map. Does not include step-expand-syntax-quotes — forms contain AST nodes (MemeSyntaxQuote, MemeRaw) for tooling access. Call step-expand-syntax-quotes separately if you need eval-ready forms.
(meme-lang.stages/run "+(1 2)")
;=> {:source "+(1 2)", :opts nil,
; :cst [...], :forms [(+ 1 2)]}
Unified CLI for meme. JVM/Babashka only.
| Command | Description |
|---|---|
meme run <file> | Run a .meme file |
meme repl | Start the meme REPL |
meme to-clj <file\|dir> | Convert .meme files to .clj (in-place) |
meme to-meme <file\|dir> | Convert .clj/.cljc/.cljs files to .meme |
meme format <file\|dir> | Format .meme files via canonical formatter (in-place by default, --stdout to print, --check for CI) |
meme compile <dir\|file...> | Compile .meme to .clj in a separate output directory (--out target/classes). Output preserves relative paths — add the output dir to :paths in deps.edn for standard require without runtime patching. |
meme inspect [--lang] | Show lang info and supported commands |
meme version | Print version |
All file commands accept directories (processed recursively) and multiple paths. to-clj, to-meme, and format accept --stdout to print to stdout instead of writing files. Use --lang to select a lang backend (default: meme).
Entry point: -main dispatches via babashka.cli. For Clojure JVM, use -T:meme (e.g., clojure -T:meme run :file '"hello.meme"').
Native value resolution. Converts raw token text to Clojure values — no read-string delegation. Consistent error wrapping and location info.
All resolvers take the raw token text and a loc map ({:line N :col M}) for error reporting:
(meme-lang.resolve/resolve-number raw loc) ;; "42" → 42
(meme-lang.resolve/resolve-string raw loc) ;; "\"hi\"" → "hi"
(meme-lang.resolve/resolve-char raw loc) ;; "\\newline" → \newline
(meme-lang.resolve/resolve-regex raw loc) ;; "#\"\\d+\"" → #"\d+"
(meme-lang.resolve/resolve-auto-keyword raw loc resolve-fn)
Resolve an auto-resolve keyword (::foo). If resolve-fn is provided, resolves at read time. Otherwise, defers to eval time via (read-string "::foo").
(meme-lang.resolve/resolve-tagged-literal tag data loc)
Resolve a tagged literal. JVM: produces a TaggedLiteral object via clojure.core/tagged-literal. CLJS: throws an error.
The meme reader fails fast on invalid input. Parse errors are thrown as ex-info exceptions with :line and :col data when available.
Common errors:
| Error message | Cause |
|---|---|
Expected :close-paren but got EOF | Unclosed ( |
Unquote (~) outside syntax-quote | ~ only valid inside ` |
(try
(meme-lang.api/meme->forms "foo(")
(catch Exception e
(ex-data e)))
;=> {:line 1, :col 4}
Error recovery is not supported — the reader stops at the first error. This is documented as future work in the PRD.
Namespace loader for .meme files. Intercepts clojure.core/load and clojure.core/load-file to handle .meme files transparently. JVM/Babashka only.
(meme.loader/install!) ;; => :installed
(meme.loader/uninstall!) ;; => :uninstalled
install! is idempotent — safe to call multiple times.
| Function | Effect | JVM | Babashka |
|---|---|---|---|
require | (require 'my.ns) searches for my/ns.meme on the classpath | Yes | No (SCI bypasses clojure.core/load) |
load-file | (load-file "path/to/file.meme") runs through the meme pipeline | Yes | Yes |
Automatic installation: run-file and the REPL's start call install! automatically. The CLI (bb meme run, bb meme repl) also installs the loader.
Precedence: When both my/ns.meme and my/ns.clj exist on the classpath, .meme takes priority.
Babashka limitation: Babashka's SCI interpreter does not dispatch require through clojure.core/load, so require of .meme namespaces is JVM-only. load-file works on both platforms. For Babashka projects that need require, use meme compile to precompile .meme to .clj.
Error infrastructure used by the scanner, reader, and resolver. Portable (.cljc).
(meme-lang.errors/source-context source line)
Extract the source line at a 1-indexed line number from source (a string). Returns the line text, or nil if out of range.
(meme-lang.errors/meme-error message opts)
Throw ex-info with a consistent error structure. opts is a map with:
:line, :col — source location (appended to message as (line N, col M)):cause — optional upstream exception:incomplete — when true, signals the REPL that more input may complete the formAll scanner, reader, and resolver errors go through this function.
(meme-lang.errors/format-error exception source)
Format an exception for display. Produces a multi-line string with:
"Error: ")^) or span underline (~~~) pointing at the column(s) — uses ^ for single-column errors, ~ for multi-column spans when :end-col is present in ex-data:secondary is present in ex-data — a sequence of {:line :col :label} maps):hint is present in ex-data)If source is nil/blank or the exception lacks :line/:col, only the prefixed message is returned.
In addition to built-in langs and EDN-loaded langs, user langs can be registered at runtime for extension-based auto-detection by run-file and the CLI.
(meme.registry/register! lang-name config)
Register a user lang. lang-name is a keyword. config is an EDN-style map — the same format as .edn lang files. Symbols are resolved via requiring-resolve, strings and keywords follow the same rules as load-edn. Pre-resolved functions are also accepted. Thread-safe — extension conflict checks are atomic.
Both :extension (string) and :extensions (vector) are accepted. Both are normalized to a single :extensions vector. The :extension key is removed after normalization.
;; Single extension
(registry/register! :prefix {:extension ".prefix"
:run "examples/languages/prefix/core.meme"
:format :meme})
;; Multiple extensions
(registry/register! :my-lang {:extensions [".ml" ".mlx"]
:run 'my-lang.run/run-string})
;; Mixed — both forms merged
(registry/register! :hybrid {:extension ".hyb"
:extensions [".hybx"]
:run 'my-lang.run/run-string})
;; Normalizes to :extensions [".hyb" ".hybx"]
(meme.registry/resolve-by-extension path)
Given a file path, find the user lang whose :extension matches. Returns [lang-name lang-map] or nil.
(meme.registry/registered-langs)
List all registered user language names (keywords).
(meme.registry/clear-user-langs!)
Clear all registered user languages. For testing.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |