Liking cljdoc? Tell your friends :D

meme clojure — Product Requirements Document

Problem

Clojure's S-expression syntax requires structural editing (paredit) to manage parenthesis nesting reliably. Without it, writing and reading deeply nested forms is error-prone: mismatched parentheses, wrong bracket types, incorrect nesting depth. These are bookkeeping errors, not semantic ones — imposed by syntax that demands manual bracket management.

Solution

meme is a complete Clojure frontend. One rule replaces S-expression nesting with readable, familiar syntax:

f(x y) — call. Head of a list written outside the parens, adjacent to ( (spacing significant).

Everything else is unchanged from Clojure. Programs run on Babashka, Clojure JVM, or ClojureScript without modification. The platform includes a reader, printer, formatter, REPL, file runner, and CLI — the CLI itself is written in .meme.

Goals

  • Human-readable Clojure. The syntax should be immediately legible to anyone who knows Clojure, Python, Ruby, or JavaScript.

  • Full Clojure compatibility. Every valid Clojure program has a meme equivalent. Every meme program produces valid Clojure forms. Data literals, destructuring, reader macros, macros, metadata — all work unchanged.

  • Self-hosting. meme code should be able to build meme itself. The CLI is the first component written in .meme.

  • Roundtrippable. meme text → Clojure forms → meme text should produce equivalent output. This enables tooling: formatters, linters, editors.

  • Portable. Core pipeline runs on Clojure JVM, ClojureScript, and Babashka. Single codebase, .cljc files.

  • Platform for guest languages. The tokenizer, pipeline, and FullForm representation are designed as a foundation for languages beyond Clojure. See doc/platform-roadmap.md.

Non-goals

  • Replacing Clojure syntax. meme is an alternative surface syntax. Developers who prefer S-expressions should keep using them.

  • Error recovery. The reader fails fast on invalid input. Partial parsing and error recovery are future work.

Target users

  1. Developers writing Clojure without paredit. Terminal, basic text editors, web forms, notebooks. meme syntax eliminates the bookkeeping that structural editors normally handle.

  2. Anyone reading Clojure code. meme is easier to scan than S-expressions — code review, diffs, logs, documentation.

  3. Agents generating Clojure. What is good for humans is good for agents. meme reduces syntax errors on the structural dimension.

Requirements

Reader

IDRequirementStatus
R1Parse f(x y) as (f x y) — head outside parens, adjacent ( required (spacing significant)Done
R5Parse def(x 42) as (def x 42)Done
R6Parse let([x 1] body) — bindings in call formDone
R7Parse for([x xs] body) — bindings in call formDone
R8Parse defn — single arity, multi-arity, docstringDone
R9Parse fn — anonymous functionsDone
R10Parse if(cond then else) as call formDone
R13Parse ns(...) with :require and :importDone
R15Parse all Clojure data literals unchangedDone
R16Parse Clojure reader macros (@, ^, #', #_, ')Done
R17Parse #?() reader conditionals natively (no read-string)Done
R18Parse defprotocol(...), defrecord(...), deftype(...), reify(...), defmulti(...), defmethod(...)Done
R19Parse Java interop: .method(), Class/static(), .-field()Done
R20Commas are whitespaceDone
R21Line/column tracking for error messagesDone
R22Portable .cljc — core reader/printer run on JVM, ClojureScript, BabashkaDone
R23Signed numbers: -1 is number, -(1 2) is call to -Done
R24#:ns{...} namespaced maps parsed natively (no read-string)Done
R25#() uses meme syntax inside, % params → fn formDone
R26run-pipeline exposes intermediate pipeline state for toolingDone
R28() is the empty list (no head required)Done
R29No S-expression escape hatch — '(...) uses meme syntax insideDone
R30Syntax-quote parsed natively — meme syntax inside `Done
R31Zero read-string delegation — all values resolved nativelyDone

Printer

IDRequirementStatus
P1Print (f x y) as f(x y)Done
P5Print (def x 42) as def(x 42)Done
P6Print (let [x 1] ...) as let([x 1] ...)Done
P7Print (for [x xs] ...) as for([x xs] ...)Done
P8Print defn with proper meme syntaxDone
P9Print if as call formDone
P11Print all Clojure data literalsDone
P12Proper indentationDone
P13Roundtrip: read then print produces re-parseable outputDone

REPL

IDRequirementStatus
RE1Read meme input, eval as Clojure, print resultDone
RE2Multi-line input: wait for balanced brackets and parensDone
RE3Run via bb memeDone

CLI

IDRequirementStatus
C1meme run <file> — run a .meme fileDone
C2meme repl — start interactive REPLDone
C3meme convert <file\|dir> — convert between .meme and .clj (by extension)Done
C4meme format <file\|dir> — normalize .meme files via canonical formatter (in-place or stdout)Done

Note: Requirement IDs are not sequential — gaps (R2–R4, R11–R12, R14, P2–P4, P10) are requirements that were merged into other IDs or removed during design iteration. IDs are stable references and are not renumbered.

Architecture

.meme file ──→ tokenizer ──→ parser ──→ Clojure forms
                 (scan)       (parse)        │
                    │            │           ▼
                  source      resolve    expander ──→ rewrite ──→ eval
               (shared line/col                │
                → offset contract)       printer ──→ .meme text
                  pipeline.contract    formatter ──→ .meme text
               (spec validation at
                stage boundaries)

The pipeline has composable stages (composed by meme.alpha.pipeline), each a ctx → ctx function with a step- prefix:

  1. step-strip-shebang — remove #! line from :source (for executable scripts). Defined in runtime/run, not part of the core pipeline.
  2. step-scan (meme.alpha.scan.tokenizer) — character stream → flat token vector. Compound forms (dispatch, syntax-quote) emit marker tokens.
  3. step-parse (meme.alpha.parse.reader) — recursive-descent parser, tokens → Clojure forms. Value resolution (numbers, strings, chars, regex, keywords, tagged literals) is delegated to meme.alpha.parse.resolve. Volatile position counter for portability. No intermediate AST — forms are emitted as standard Clojure data. No read-string delegation. Accepts an optional :parser in opts for guest language plug-in parsers.
  4. step-expand-syntax-quotes (meme.alpha.parse.expander) — syntax-quote AST nodes → plain Clojure forms. Only needed before eval, not for tooling. meme.alpha.pipeline/run intentionally omits this stage, returning AST nodes for tooling access.
  5. step-rewrite (meme.alpha.rewrite) — apply rewrite rules to :forms. No-op if no :rewrite-rules in opts. Used by run-string for guest language transforms.

Stage boundaries are validated by meme.alpha.pipeline.contract (clojure.spec) when contract/*validate* is bound to true.

The printer pattern-matches on form structure to reverse the transformation. It detects special forms and produces their meme syntax equivalents.

All # dispatch forms (#?, #?@, #:ns{}, #{}, #"", #', #_, #(), tagged literals) and syntax-quote (`) are parsed natively with meme rules inside. No opaque regions.

Known limitations

  • ClojureScript: some value types unsupported. Tagged literals (#uuid, #inst), reader conditionals (#?, #?@), namespaced maps (#:ns{}), char literals, ratio literals, BigInt/BigDecimal are JVM/Babashka only. The core call rule and all standard forms work on ClojureScript.

  • Reader conditionals and roundtrips. The printer emits meme syntax inside #?(...) natively. By default, meme's reader evaluates #? to the matching platform's branch at read time. Pass {:read-cond :preserve} to meme->forms to return ReaderConditional objects instead, enabling lossless clj->meme->clj roundtrips of .cljc files.

  • Nesting depth limit. The parser enforces a maximum nesting depth of 512 levels. Exceeding this produces a clear error. This prevents stack overflow on recursive descent.

Completed work (post-initial release)

  • Syntax highlighting grammars. TextMate grammar in vscode-beme/, Tree-sitter grammar in tree-sitter-beme/ (both in the beme-lang org). Both cover .beme and .meme extensions.

  • Platform / guest language system. See doc/platform-roadmap.md and doc/LANGBOOK.md. Includes:

    • Rewrite engine (meme.alpha.rewrite) — pattern matching, rule application, bottom-up rewriting to fixpoint. defrule, ruleset macros.
    • Platform registry (meme.alpha.platform.registry) — register! a guest language with :extension, :prelude, :rules, :parser. run-file auto-detects guest languages from file extension.
    • Pipeline integrationstep-rewrite stage, pluggable :parser in step-parse, :prelude/:rewrite-rules/:rewrite-max-iters options in run-string.
    • Example languages in examples/languages/: calc, prefix, superficie.

Platform requirements

IDRequirementStatus
PL1Rewrite engine: pattern matching (?x, ??x), substitution, bottom-up rewritingDone
PL2defrule, defrule-guard, ruleset macros for rule definition (JVM)Done
PL3Language registry: register!, resolve-lang, lang-configDone
PL4run-file auto-detects guest language from file extensionDone
PL5run-string accepts :prelude, :rewrite-rules, :rewrite-max-itersDone
PL6Pluggable parser: :parser option in step-parse for guest language parsersDone
PL7step-rewrite pipeline stage applies rules after expansionDone
PL8Pipeline contract: spec validation at stage boundaries (pipeline.contract)Done

Future work

  • Error recovery: partial parsing for editor integration. This would require the parser to accumulate errors into a vector rather than throwing, return partial ASTs with error nodes, and add try/catch wrappers in parse-form that advance to resynchronization points (closing delimiters or newlines). This is a significant architectural change and should be its own project.
  • nREPL middleware

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