All notable changes to meme-clj will be documented in this file.
The format is based on Keep a Changelog.
A reorganization release. No breaking changes to .meme syntax or runtime behavior; most of the work is in documentation, API hygiene, and internal boundaries.
meme.registry imports no concrete langs. Each lang's api namespace calls register-builtin! at its own load time, and the CLI triggers registration by explicitly requiring each lang it ships with. Dissolves the old registry↔lang cycle and four requiring-resolve workarounds.meme.registry and meme.loader are now documented as shared infrastructure peer to meme.tools.*, not as a strict "above" tier over meme-lang.*. meme-lang.api requiring meme.registry (for self-registration) and meme-lang.run requiring meme.loader (for auto-install) are intentional; the CLAUDE.md tier table has been updated to match.stage-contracts; check-contract! runs at entry and throws :meme-lang/pipeline-error with the missing key(s) when pipelines are miscomposed, instead of deep NPEs.meme.tools.parser exposes only trivia-pending? to language grammars; other engine internals are no longer part of the grammar-author contract.char-code consolidated into meme.tools.lexer — duplicate helper in meme-lang.lexlets removed.meme.registry/register-string-handler! — lang-agnostic hook for resolving string values (e.g. :run "prelude.meme") in lang-map slots. Meme installs its own :run handler at load time. Replaces the previous hardcoded requiring-resolve of meme-lang.run/run-string inside the registry.meme-lang.run/run-file opts — :install-loader? (default true; pass false to skip auto-install of meme.loader) and :resolve-lang-for-path (extension-based lang dispatch hook, injected by the CLI).meme-lang.repl/start opt — :install-loader? mirrors the above.meme.tools.parser and meme.tools.lexer using a minimal synthetic calculator grammar, covering precedence (left/right-assoc), EOF recovery, max-depth, trivia attachment, :when predicate gating, and all scanlet/parselet factories.run-string, run-file, and repl/start auto-install meme.loader. require/load-file of .meme namespaces work in the common programmatic case, not just via the CLI. Hosts that own their own clojure.core/load interception opt out via :install-loader? false.clojure.core/load — no cosmetic noise on REPL start.src/meme_lang/cst_reader.cljc). Reader allowed one more level of recursion than the parser's limit (> → >=). Behavior now matches the parser at exactly max-parse-depth levels.meme.tools.lexer cross-platform bug — digit?/ident-start?/ident-char? claimed portability but returned wrong results on CLJS because (int ch) on a single-char string returns NaN|0 = 0 rather than the code point. Fixed with a char-code helper that uses .charCodeAt on CLJS. Meme's grammar was unaffected (its own meme-lang.lexlets had the same pattern); calc-lang relied on the generic helpers and would have silently failed on CLJS.meme-lang.api/to-clj and to-meme marked ^:no-doc. These are CLI-dispatch adapters (they always apply :read-cond :preserve); library callers should use meme->clj / clj->meme directly.meme.registry/clear-user-langs! and registered-extensions marked ^:no-doc — internal plumbing used by tests and the loader respectively.meme-lang.parselets/reader-cond-extra made private (used only within the file).meme.tools.parser/make-engine, meme.tools.lexer/{digit?, ident-char?, newline-consumer}, meme.config/config-filename.clj->forms — no longer relies on catching StackOverflowError at CI JVM sizes; walks the returned forms against max-parse-depth instead.fuzz/ — corpus and crash artifacts moved out of the project root.compile sections to the README.load-file interception — (load-file "path/to/file.meme") runs through the meme pipeline on both JVM and Babashka.meme compile CLI command — compiles .meme to .clj into a separate output directory (--out target/classes by default) so .meme namespaces work via standard require without runtime patching. Primary use: Babashka projects that need require (SCI bypasses clojure.core/load, so runtime interception isn't enough)..meme-format.edn config — meme format discovers config by walking up from CWD. Schema: :width, :structural-fallback?, :form-shape (symbol→built-in alias), :style (partial canon override). Strict EDN, unknown tags rejected, unknown keys warn.meme-lang.formatter.canon/style carries the slot-keyed opinions.meme-lang.form-shape) — semantic decomposition of special forms into named slots (:name, :doc, :params, :bindings, :clause, :body, :dispatch-val, etc.). Lang-owned: each lang carries its own registry. The printer dispatches on slots, not form names. Documented as a public contract in doc/form-shape.md.with-structural-fallback wraps a registry so user macros shaped like defn (name + params vector) or let (leading bindings vector) inherit canonical layout.:slot-renderers composes over printer/default-slot-renderers via plain map merge; formatters accept :style override in opts for project-level tweaks. The canon style collapsed from ~60 form-keyed entries to 11 slot-keyed entries.let/loop/for/doseq/...) on the head line; bindings render as pairs per line with columnar alignment.defn/defn-/defmacro) keep name + params on the head line; always space after ( for definition forms.case, cond, condp render their bodies as paired clauses.meme.loader): intercepts clojure.core/load to find .meme files on the classpath. Auto-installed by run-file and REPL start. install!/uninstall! for manual control. require in .meme code finds both .meme and .clj namespaces; .meme takes precedence when both exist.register! accepts both :extension (string) and :extensions (vector); both are normalized to :extensions [...]. Built-in meme lang registers .meme, .memec, .memej, .memejs.clojure.*, java.*, javax.*, cljs.*, nrepl.*, cider.* namespaces cannot be shadowed by .meme files on the classpath.uninstall! throws when called from within a lang-load (prevents .meme code from disabling the loader mid-execution).#::{} bare auto-resolve: namespaced maps with empty alias (#::{:a 1}) are now accepted, matching Clojure's behavior. Keys stay unqualified; qualification deferred to eval time.doc/red-team/report.md — 71 adversarial hypotheses tested across parser, value resolution, expansion, printer, loader, registry, and CLI.doc/design-decisions.md — analysis of lists, homoiconicity, and M-expressions.meme.tools.* (generic parser/render), meme-lang.* (meme language), meme.* (CLI/registry/loader). The Pratt parser is fully data-driven via grammar spec.:meme/* to :meme-lang/*, with descriptive names. :meme/ws → :meme-lang/leading-trivia, :meme/sugar → :meme-lang/sugar, :meme/order → :meme-lang/insertion-order, :meme/ns → :meme-lang/namespace-prefix, :meme/meta-chain → :meme-lang/meta-chain, :meme/bare-percent → :meme-lang/bare-percent, :meme/splice → :meme-lang/splice. This separates meme-lang metadata from the generic meme.tools namespace, preventing collision with both user metadata and future languages built on meme.tools.*.register! conflict check is atomic: extension validation moved inside swap! callback, preventing TOCTOU race on concurrent registrations.process-files reads source via slurp once and passes content to transform, eliminating TOCTOU between read and write.meme-file? and swap-ext consult the registry for all registered extensions, not just hard-coded .meme.strip-shebang correctly handles CRLF (\r\n) line endings.clj->forms catches StackOverflowError on deeply nested Clojure input and rethrows as ex-info.%N param OOM: anonymous function params capped at %20 (was ~10^11 → instant heap exhaustion). Matches Clojure's LispReader limit.:/foo keyword accepted: leading-slash keywords now rejected, matching Clojure's reader.:shebang atom in CST reader: double-shebang files no longer produce "Unknown atom type" error.0x: produces "Empty hex literal" instead of leaking Java's "Zero length BigInteger" message.:meme/order stale metadata: printer validates order vector count against set size; falls back to unordered when stale.:invalid token instead of two confusing errors per surrogate half.#_ discard tokens: capture start position before advance, producing correct non-zero-length spans.reader_test.cljc converted to active tests or documented design decisions. Two stale comments corrected (duplicate keys and %0 ARE rejected by the current pipeline).:meme/* internal metadata keys: replaced by :meme-lang/* namespaced equivalents with descriptive names. The :meme/ namespace is now reserved for generic tooling.meme.* (no more meme.alpha.*)get-lang: fixed variable shadowing that broke "Available langs" display\uNNNN with invalid hex digits now throws instead of silently producing wrong valuesload-resource-edn gives clear message when resource not found on classpath; clj->forms preserves source context in error ex-data; removed inconsistent trailing period in CLJS tagged-literal errormeme.emit.printer (tagged literal field access)meme.lang, meme.forms, meme.emit.render; fixed run-stages "full pipeline" misrepresentation; fixed indentation on clj-> function docstringspipeline namespace references in api.md, PRD.md, design-decisions.md; fixed wrong REPL launch command in api.md; removed phantom lang.util from platform tiers; corrected convert lang count in PRD.md.clj shim + .meme bootstrap with plain Clojure; generic dispatcher delegates to lang map functions; CLI opts (e.g. --width) pass through to langsmeme.convert module: removed in favor of :to-clj / :to-meme commands on lang maps. Use ((:to-clj (meme.lang/resolve-lang :meme-rewrite)) src) for multi-lang conversion, or meme.core/meme->clj for the classic path.meme.core/version: runtime version access — @meme.core/version returns "1.0.0":classic, :rewrite, :ts-trs in meme.lang/resolve-lang now emit a deprecation warning. Use :meme-classic, :meme-rewrite, :meme-trs instead.match-pattern now matches map patterns by key ({:k ?x} matches {:k 42}) and set patterns by element presencerewrite-once descends into maps (excluding records) and sets, so rules match subexpressions inside map values/keys and set elementsf(x)(y) → ((f x) y) — fixed left-to-right scan in rewrite-level with re-check after matchformat(format(x)) == format(x).meme fixture covering Clojure/meme code in comments, multiple semicolons, commented-out code, mid-expression and trailing commentsbenchmark_test now exercises classic, rewrite, and ts-trs across 11 fixtures and 7,526 vendor forms"hello\ now reports "Incomplete escape sequence" instead of misleading "Unterminated string"" inside regex now escaped in output, matching printer.cljc behavior:meme.opts/prelude corrected from string? to (s/coll-of any?) matching runtime type (vector of forms)gen-meme-text set generator now deduplicates elements, fixing intermittent prop-meme-text-roundtrip failurememe convert --lang meme-classic|meme-rewrite selects the conversion lang; meme inspect --lang shows lang infomeme.convert): single dispatch point for all three langsbenchmark_test.clj): benchmarks all three langs across 11 meme fixtures and 7,526 vendor forms from 7 real-world Clojure librariesregister! API for guest languages with custom preludes, rewrite rules, and parsers (meme.lang)?x/??x pattern variables, cycle detection, and fixed-point iteration (meme.rewrite)meme.rewrite.tree)meme.stages.contract)nil(1 2) → (nil 1 2). Any value can be a head. Previously these were rejected artificially.clojure.core// now reads as one symbol (namespace clojure.core, name /). Previously split into two tokens.M suffix, BigInt N suffix, ##NaN/##Inf/##-Inf, tagged literals, named chars (\newline etc.), pr-str fallback for UUID/Date:read-cond :preserve during conversion, preserving #?(:clj ...) branches in outputstep-expand-syntax-quotes before eval, matching the user-code pathbuild-tree now validates expected delimiters after #? and #:ns prefixesdoc/development.mdMemeRaw, MemeSyntaxQuote, and other AST node defrecords satisfy (map? x), causing silent mishandling in expand-sq, normalize-bare-percent, find-percent-params, pp, and max-percent-n. All dispatch sites now guard with forms/raw?, forms/syntax-quote?, etc. before the (map? form) branch.#?@(:clj [2 3]) inside a collection now correctly splices elements ([1 #?@(:clj [2 3]) 4] produces [1 2 3 4], not [1 [2 3] 4]). Non-sequential splice values produce a clear error.+42N and +3/4 now parse correctly. BigInteger constructor rejects leading +; sign is now stripped before construction (matching hex/octal/radix branches).nil(x), true(x), false(x) are now valid meme syntax, producing the lists (nil x), (true x), (false x). The syntactic rule f(args) → (f args) applies uniformly regardless of head type.x `` now correctly produces double-quoting (code that generates the inner expansion), matching Clojure's behavior. Previously the inner expansion was returned directly, losing one nesting level.expand-sq, expand-syntax-quotes, expand-forms) moved from meme.parse.reader to new meme.parse.expander namespace.strip-internal-meta) and percent-param-type extracted to meme.forms to prevent drift between reader and printer.test-cljs to deploy job dependencies).Initial public alpha release.
:meme/sugar metadata preservationMemeSyntaxQuote), expanded before evalMemeRaw) for numbers/chars/strings with alternate notation:read-cond :preserve option for lossless reader conditional roundtrips:incomplete continuation protocol.memeCan 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 |