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 thin syntactic lens over Clojure. 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. meme is a reader, not a language. It emits standard Clojure forms that run on Babashka, Clojure JVM, or ClojureScript without modification.

Goals

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

  • Eliminate paren-matching errors. The syntax makes it structurally impossible to produce the most common classes of S-expression errors.

  • Zero runtime cost. meme is a compile-time (read-time) transformation. The output is standard Clojure forms. No runtime library, no overhead.

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

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

  • Portable. The reader and printer run on Clojure JVM, ClojureScript, and Babashka. Single codebase, .cljc files, no platform-specific code.

Non-goals

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

  • New semantics. meme adds no language features. No new data types, no new evaluation rules, no new special forms. If it doesn't exist in Clojure, it doesn't exist in meme.

  • IDE integration. Not in scope for v1. The REPL and file-based workflow are sufficient.

  • Performance optimization. The reader should be fast enough for interactive use. It does not need to compete with Clojure's reader on throughput for large codebases.

  • 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 pprint (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 text ──→ tokenizer ──→ grouper ──→ parser ──→ Clojure forms ──→ eval
               (scan)        (group)     (parse)          │
                  │              │          │              ▼
                  └──── source ──┘       resolve   printer ──→ .meme text
                   (shared line/col                 pprint ──→ .meme text
                    → offset contract)

The reader is a three-stage pipeline (composed by meme.alpha.pipeline):

  1. Scan (meme.alpha.scan.tokenizer) — character stream → flat token vector. Compound forms (dispatch, syntax-quote) emit marker tokens.
  2. Group (meme.alpha.scan.grouper) — pass-through stage (all forms are now parsed natively; the grouper is retained for pipeline symmetry).
  3. 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.

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. However, clj->meme roundtrips of ReaderConditional objects are lossy because meme's reader evaluates #? to one branch's value at read time — the conditional structure is not preserved through the full roundtrip.

  • 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.

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.
  • Syntax highlighting grammars (TextMate, Tree-sitter)
  • 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