cljgo

module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: MIT

README

cljgo

CI Release Go Reference Go Clojure core.async clojure-test-suite License

muthuishere.github.io/cljgo — docs, examples, and the live status board.

Clojure hosted on Go: a compiler (written in Go) that AOT-emits plain Go source — the ClojureScript model with Go as the JavaScript — plus a tree-walk evaluator that is the REPL and the macro engine. The same source runs interpreted at the prompt and compiles to a single static native binary, with byte-identical output on both paths.

Why

  1. Universal interop — any Go module importable and callable with zero bindings; the Go ecosystem is the standard library. C via cgo modules and purego FFI.
  2. Full REPL-driven development — live re-def, defmacro at the prompt, nREPL for CIDER/Calva.
  3. Faithful Clojure principles — persistent data structures, macros, seqs, vars. Clojure is first-class: nothing cljgo adds may shadow or change clojure.core semantics.
  4. High performance in both modes — a feature, gated in CI like tests, not asserted.
  5. Single-file deploymentcljgo build produces one static binary (5.3 MB for hello, ~5 ms startup), no JVM, no runtime install.

Install

go install github.com/muthuishere/cljgo/cmd/cljgo@latest

Or grab a prebuilt binary for your platform from the latest release (macOS/Linux/Windows, amd64 + arm64).

cljgo repl, cljgo run and Go interop work from the binary alone, with no Go toolchain installed. cljgo build additionally needs the Go toolchain. From v0.2.0, that is the whole story: a release binary pins the published runtime module in the generated go.mod (require github.com/muthuishere/cljgo v<version>, ADR 0028), and the first build fetches it from the Go module proxy once per machine (~1 MB, a few seconds). (v0.1.0 binaries predate that and need a repo checkout via CLJGO_SRC.)

Quickstart

;; hello.clj
(require-go '[strings])
(require-go '[strconv])

(println (strings/ToUpper "hello from go's stdlib"))
(println "Atoi! ->" (strconv/Atoi! "42"))   ; unwraps, or throws
(println "Atoi  ->" (strconv/Atoi "oops"))  ; => [0 <error>], errors-as-values
$ cljgo run hello.clj        # interpreted
$ cljgo build hello.clj      # -> ./hello, a static native binary
$ ./hello                    # byte-identical output

The Go ecosystem is the standard library: (require-go '[net/http :as http]) and call it — no bindings, no wrappers, the Go toolchain is the classpath.

Projects, dependencies, publishing:

$ cljgo new myapp            # generate a project: --template lib (default) | cli | web
$ cljgo test                 # run the project's tests (clojure.test)
$ cljgo build                # resolve declared deps (build.cljgo + build.lock.edn), compile
$ cljgo publish go           # publish the library to the Go module ecosystem — or `clojars`

Sources may be .clj, .cljg, or .cljgo (ADR 0055); dependency resolution and lockfiles are ADR 0052/0053, publishing to both ecosystems is ADR 0054.

Editor REPL: cljgo nrepl, then connect Calva ("Connect to a running REPL") or CIDER (cider-connect-clj) to the printed port — .nrepl-port makes it auto-discoverable.

Errors carry registered codes with explain pages: cljgo check file.clj --json for structured diagnostics, cljgo explain A2004 for the long-form page (ADRs 0015/0048).

Status

Working REPL and native compiler. The same source runs interpreted at the prompt and AOT-compiles to a static Go binary — byte-identical output on both paths (a dual-harness conformance suite, 416 oracle-cited files, enforces this on every commit; a REPL↔binary divergence is a release blocker).

Against the jank clojure-test-suite (upstream @164a4b3, unmodified): 238/242 files passing (98.3%), with 242/242 vars resolved (100%), 0 failures and 4 errors. Run cljgo suite to reproduce.

Those 4 are dialect registration, not broken semantics: abs, add-watch, short and reduce carry reader conditionals with no :default branch, so a runtime the suite has never heard of reads them as nothing. Adding a :cljgo branch — the same mechanism :cljr / :lpy / :phel already use — takes the suite to 242/242 (100%); those branches are not upstreamed yet, so the published number is what the suite gives as it ships (analysis: docs/suite-upstream.md).

Area State What landed
Language core (M0–M2) Reader (full syntax-quote, tagged literals, reader conditionals — ADRs 0036/0050), macros, defmacro at the prompt, protocols/deftype/defrecord/reify (ADRs 0020/0049), numeric tower (0029/0032), JVM-compatible hashing (0051), cljgo build → native binary
Go interop (M3) Zero-ceremony, both modes — require-go, package fns/consts, (T,error)[v err], ! unwrap-or-throw, members (.Method r …) / (.-Field r) / set!, ctors (pkg/T. {…}), (go/new T)
core.async Over real goroutines — no CPS rewrite. 55 publics = every non-deprecated, non-internal var of JVM core.async 1.6.681, including alts!/alt!, timeout, transducers, mult/pub/mix/pipe, pipeline(-blocking/-async) (ADR 0040; audit: docs/core-async-audit-2026-07.md)
Satellite namespaces clojure.string · set · edn · walk · zip · data · repl · pprint complete against the 1.12.5 oracle; clojure.test complete (39 oracle vars)
Result/Option ok/err/just/none + unwrap/and-then/map-ok + let?, #cljgo/ok literals (ADR 0014)
Diagnostics Banded error codes + explain pages, cljgo check --json, cljgo explain <code> (ADRs 0015/0048)
nREPL cljgo nrepl — Calva/CIDER connect; babashka's 13-op surface, per-session *ns*/*1/*out* streaming, .nrepl-port (ADR 0031)
Projects & toolkit cljgo new (real runnable templates: lib/cli/web, ADR 0047), dependency resolution + lockfile (build.cljgo/build.lock.edn, ADRs 0052/0053), cljgo publish go|clojars (0054), .clj/.cljg/.cljgo (0055); app scaffolding cljgo dev/config/routes (ADR 0041 T0/T1)
AOT core Compiled binaries link the compiled core.clj, never the interpreter — pkg/eval 155 → 0 symbols in the link set (ADR 0046)
Perf campaign ADRs 0063–0067: chunk-aware seq ops, IFn2 reduce seam, direct-call emission, sealed-core guard elision, int64 numeric inference — emitted factorial ~35× → 4.8× handwritten Go (details)
Next ADR 0067 follow-ups (float64, multi-arity/variadic specialization, capturing-closure lift); reduce/transducers vs babashka's core (the two rows still lost); app framework T2 (ADR 0041); C FFI purego (ADR 0044, proposed, spike S21); batteries direction (ADRs 0056–0062, ratified on feat/batteries — decisions recorded, not shipped)

clojure.core is complete as of 2026-07-23: 632 of 679 oracle vars (93%) implemented with oracle-exact, conformance-frozen semantics; the other 47 are JVM bytecode/classloader machinery (proxy, gen-class, java.util.stream, compiler knobs) each documented with a one-line reason — no third bucket. Every satellite namespace (string/set/edn/walk/zip/data/ repl/pprint/test) is 100%. The per-var ledger is docs/fundamentals-audit-2026-07.md.

Performance

Performance is priority 4 and gated like conformance, not asserted. On let-go's own benchmark suite (Apple M5 Pro, go1.26.3, wall-clock incl. startup), the AOT binary (cljgo build) wins every recursion and data-structure row outright and ties on startup:

Benchmark cljgo let-go babashka clojure JVM
startup 5.0 ms 5.0 ms 9.7 ms 289.2 ms
fib 24.7 ms 1.25 s 1.14 s 419.1 ms
loop-recur 5.4 ms 36.6 ms 37.8 ms 397.5 ms
persistent-map 9.4 ms 12.9 ms 13.0 ms 385.2 ms
reduce 26.0 ms 22.8 ms 20.0 ms 302.5 ms

reduce/transducers still trail the two purpose-built cores — closer than before, but honestly lost. The emitted-vs-handwritten-Go factorial gate measures ~4.8× (it was ~35× before the 2026-07-23 campaign, ADRs 0063–0067); two budgets — interpreter boot and that ratio — run inside plain go test ./....

Full numbers, methodology, the head-to-head across all runtimes, and the campaign history: docs/performance.md (reproduce with bash benchmark/run.sh). Building an AOT binary is one command: cljgo build -o hello hello.clj.

Development

Authority chain: docs/adr/ (decisions) › design/00-architecture.md (contracts + M0–M5 roadmap) › design/01–07 (component internals) › openspec/ (active change proposals). Process for non-trivial work: ADR → OpenSpec propose/design → apply.

Gates before every commit:

go build ./... && go vet ./... && gofmt -l pkg cmd conformance templates && go test ./...
pkg/lang     runtime (persistent data structures, vendored from Glojure)
pkg/corelib  Go-native core builtins (ADR 0043)
pkg/reader   pkg/ast   pkg/analyzer   pkg/eval   pkg/repl   pkg/emit
pkg/coreaot  the compiled clojure.core a built binary links (ADR 0046)
pkg/deps     dependency resolution + lockfile (ADR 0052)
cmd/cljgo    CLI (repl · nrepl · run · build · new · test · publish · suite · check · explain · …)
core/        core.clj + satellite namespaces — Clojure-in-Clojure
templates/   real, runnable project templates `cljgo new` embeds (lib · cli · web)
conformance/ dual-harness tests (eval + compiled), oracle-cited vs JVM Clojure
design/      architecture + component design docs
docs/adr/    decision log        openspec/   spec-driven change proposals

Toolchain: Go 1.26.

Credits

cljgo stands on work by people who solved the hard parts first.

  • Clojure — Rich Hickey and contributors. The language, and cljgo's specification: every semantic behavior in conformance/ is verified against real JVM Clojure as the oracle, and the Java source (LispReader.java, Compiler.java, PersistentVector.java, PersistentHashMap.java) is the reference the reader, analyzer and data structures were built from.
  • Glojure — the runtime under pkg/lang is a hard fork of Glojure's persistent data structures, seqs, symbols, keywords and vars (v0.6.8). Roughly 17k lines that would have taken months to write from scratch. It stays EPL-1.0; our surgery on it is logged in pkg/lang/PROVENANCE.md.
  • Elvish — the persistent vector in pkg/lang/internal/persistent/vector is a port from the Elvish shell.
  • cljs2go — Håkan Råberg's 2015 Clojure→Go experiment. Read as reference for the emitter's per-op emission strategy and AFn machinery; proof the reader→analyzer→emitter split works with Go as a target. No code taken.
  • let-go — reference for treating Go channels and goroutines as first-class Clojure concurrency rather than reimplementing core.async's CPS transform. No code taken.
  • ClojureScript — the model this project follows: a compiler that emits host source, with the AST "op" vocabulary cljgo's analyzer keeps.

License

  • cljgo's own code — MIT (see LICENSE).
  • pkg/lang/ — Eclipse Public License 1.0, as vendored from Glojure. The MIT grant does not extend to it.

NOTICE has the full breakdown of which license covers what.

Directories

Path Synopsis
cmd
cljgo command
bri.go — the project CLI surface (ADR 0041, ADR 0047, openspec app-framework T0/T1):
bri.go — the project CLI surface (ADR 0041, ADR 0047, openspec app-framework T0/T1):
gencore command
Command gencore AOT-compiles cljgo's own core — core.clj and every embedded .cljg boot source — into the Go packages under pkg/coreaot (ADR 0046, AOT-core piece 3).
Command gencore AOT-compiles cljgo's own core — core.clj and every embedded .cljg boot source — into the Go packages under pkg/coreaot (ADR 0046, AOT-core piece 3).
parity.go — the ADR 0053 decision 4 dual-mode host-resolution parity comparator, extending the ADR 0007 dual harness (conformance_test.go + compiled_test.go).
parity.go — the ADR 0053 decision 4 dual-mode host-resolution parity comparator, extending the ADR 0007 dual harness (conformance_test.go + compiled_test.go).
bri.go — embedded sources for the bri application framework namespaces (ADR 0041, openspec app-framework T1).
bri.go — embedded sources for the bri application framework namespaces (ADR 0041, openspec app-framework T1).
docs
diagnostics
Package diagnostics embeds the diagnostic explain pages and the append-only registry lock snapshot so pkg/diag can serve Explain(code) from the binary (ADR 0015; design.md D2).
Package diagnostics embeds the diagnostic explain pages and the append-only registry lock snapshot so pkg/diag can serve Explain(code) from the binary (ADR 0015; design.md D2).
pkg
analyzer
Package analyzer turns read forms into pkg/ast nodes (design/03 §2–§5).
Package analyzer turns read forms into pkg/ast nodes (design/03 §2–§5).
ast
Package ast defines the AST produced by the analyzer (design/03 §1, design/00 §4.1): one uniform *Node with an integer Op tag and a typed per-op payload struct in Sub.
Package ast defines the AST produced by the analyzer (design/03 §1, design/00 §4.1): one uniform *Node with an integer Op tag and a typed per-op payload struct in Sub.
bri
auth.go — bri.auth's Go half: the S44-blessed security primitives.
auth.go — bri.auth's Go half: the S44-blessed security primitives.
build
Package build is the Zig-style build system (ADR 0021, design/08 §1).
Package build is the Zig-style build system (ADR 0021, design/08 §1).
coreaot
Package coreaot is cljgo's core, AOT-compiled: one Go package per embedded boot source (core.clj, numeric.cljg, … — core.BootSources()), each a Load() that runs that source's top-level forms.
Package coreaot is cljgo's core, AOT-compiled: one Go package per embedded boot source (core.clj, numeric.cljg, … — core.BootSources()), each a Load() that runs that source's top-level forms.
corelib
aot_stubs.go — what the interpreter-coupled builtins do in a binary that has no interpreter (ADR 0046 §5).
aot_stubs.go — what the interpreter-coupled builtins do in a binary that has no interpreter (ADR 0046 §5).
deps
Package deps implements ADR 0052 dependency resolution for cljgo: a global content-addressed cache (keyed by identity, verified by content), a deterministic committed lockfile (build.lock.edn), a resolver that reads the lock and a dependency's declarative manifest as DATA (never evaluating a dependency's build fn), a hard-error version-conflict merge for Go modules (no MVS, no solver), and a default-deny purity gate with separate :ffi/:cgo switches.
Package deps implements ADR 0052 dependency resolution for cljgo: a global content-addressed cache (keyed by identity, verified by content), a deterministic committed lockfile (build.lock.edn), a resolver that reads the lock and a dependency's declarative manifest as DATA (never evaluating a dependency's build fn), a hard-error version-conflict merge for Go modules (no MVS, no solver), and a default-deny purity gate with separate :ffi/:cgo switches.
diag
Package diag implements the structured-diagnostics data model of ADR 0015 (docs/adr/0015-structured-diagnostics-introspection.md) as settled in openspec/changes/structured-diagnostics/design.md D1/D2: the Diagnostic value, the append-only banded error-code registry, Explain pages, and a best-effort adapter from today's error values.
Package diag implements the structured-diagnostics data model of ADR 0015 (docs/adr/0015-structured-diagnostics-introspection.md) as settled in openspec/changes/structured-diagnostics/design.md D1/D2: the Diagnostic value, the append-only banded error-code registry, Explain pages, and a best-effort adapter from today's error values.
emit
Package emit is the Go source emitter (design/04, design/00 §4, ADR 0001): it consumes the analyzer's AST — never re-analyzing, never holding private special-form knowledge (ADR 0002) — and writes plain Go statements into one generated main package, gated through go/format.Source.
Package emit is the Go source emitter (design/04, design/00 §4, ADR 0001): it consumes the analyzer's AST — never re-analyzing, never holding private special-form knowledge (ADR 0002) — and writes plain Go statements into one generated main package, gated through go/format.Source.
emit/rt
Package rt is the runtime bootstrap for cljgo-emitted binaries.
Package rt is the runtime bootstrap for cljgo-emitted binaries.
eval
asyncload.go — the lazy lib provider for clojure.core.async's macro half (ADR 0040 #5, openspec core-async-first-class T1).
asyncload.go — the lazy lib provider for clojure.core.async's macro half (ADR 0040 #5, openspec core-async-first-class T1).
lang/internal/goid
Package goid returns the current goroutine's ID.
Package goid returns the current goroutine's ID.
lang/internal/identity/pkga
Package pkga simulates one separately-compiled emitted package that hoists keyword literals to package-level vars, exactly as the cljgo emitter will (design doc 00 §4.4).
Package pkga simulates one separately-compiled emitted package that hoists keyword literals to package-level vars, exactly as the cljgo emitter will (design doc 00 §4.4).
lang/internal/identity/pkgb
Package pkgb simulates a second, independently-compiled emitted package interning the same keyword literals as pkga.
Package pkgb simulates a second, independently-compiled emitted package interning the same keyword literals as pkga.
lang/internal/persistent/vector
Package vector implements persistent vector.
Package vector implements persistent vector.
lang/internal/seq
Package seq provides a definition of an internal Seq interface for use across packages.
Package seq provides a definition of an internal Seq interface for use across packages.
nrepl
Bencode codec for nREPL's wire format (ADR 0031: own the codec — everything nREPL puts on the wire in ~150 lines, zero external deps).
Bencode codec for nREPL's wire format (ADR 0031: own the codec — everything nREPL puts on the wire in ~150 lines, zero external deps).
publish
clojars.go — the `publish clojars` producer (ADR 0054 dec 1, 3).
clojars.go — the `publish clojars` producer (ADR 0054 dec 1, 3).
reader
Package reader implements the Clojure reader: UTF-8 text in, pkg/lang persistent data structures out, with :file/:line/:column/ :end-line/:end-column metadata on every IObj form.
Package reader implements the Clojure reader: UTF-8 text in, pkg/lang persistent data structures out, with :file/:line/:column/ :end-line/:end-column metadata on every IObj form.
repl
Package repl is the REPL driver of design/03-analyzer-eval.md §7b: a loop of Read (pkg/reader) → Analyze+Eval (pkg/eval) → bind *1 *2 *3 (and *e on error) → print via pr-str, over an injected reader/writer pair.
Package repl is the REPL driver of design/03-analyzer-eval.md §7b: a loop of Read (pkg/reader) → Analyze+Eval (pkg/eval) → bind *1 *2 *3 (and *e on error) → print via pr-str, over an injected reader/writer pair.
version
Package version is the single source of truth for cljgo's version, the Go toolchain hosting it, and the Clojure language level it targets.
Package version is the single source of truth for cljgo's version, the Go toolchain hosting it, and the Clojure language level it targets.
spikes
s31-impure-dep-go-modules command
Spike S31 driver — exercises pkg/emit.SynthGoMod with MERGED require sets, simulating what a `dep` verb would hand it once cljgo libraries can depend on cljgo libraries.
Spike S31 driver — exercises pkg/emit.SynthGoMod with MERGED require sets, simulating what a `dep` verb would hand it once cljgo libraries can depend on cljgo libraries.
s34-transitive-purity-validator command
Spike S34 driver — ONE transitive walk that classifies every reachable namespace of a cljgo library by purity, via PLUGGABLE taint predicates, and derives BOTH a whole-library publish gate and a per-namespace use gate from the same classification map.
Spike S34 driver — ONE transitive walk that classifies every reachable namespace of a cljgo library by purity, via PLUGGABLE taint predicates, and derives BOTH a whole-library publish gate and a per-namespace use gate from the same classification map.
Package templates carries the project templates `cljgo new` generates from (ADR 0041, ADR 0047, openspec app-framework T0) — as REAL FILES on disk, not Go string literals: a template that is a string literal is never compiled, never linted, never run, and rots the first time an API shifts.
Package templates carries the project templates `cljgo new` generates from (ADR 0041, ADR 0047, openspec app-framework T0) — as REAL FILES on disk, not Go string literals: a template that is a string literal is never compiled, never linted, never run, and rots the first time an API shifts.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL