Morgan And Grand Iron Clojure
A Clojure compiler targeting the Common Language Runtime (.NET). MAGIC compiles Clojure to MSIL bytecode, enabling Clojure to run in Unity (including IL2CPP/iOS builds) without the DLR.
This monorepo bundles the six MAGIC repositories (magic, mage, Clojure.Runtime, Magic.Runtime, nostrand, Magic.Unity) originally created by Ramsey Nasser and contributors (2014-2023).
Flybot uses MAGIC to ship Clojure code on Unity. We consolidated the six repositories into this single repository using git filter-repo (preserving all commit history and authorship) to make maintenance easier and to streamline contributions, bug fixes, and new features, both for our team and for anyone else building on MAGIC.
This is not a replacement of Ramsey's MAGIC. It's the version Flybot maintains.
The JVM Clojure compiler targets the Java Virtual Machine. MAGIC targets .NET instead, taking full advantage of the CLR's features to produce better bytecode. This enables Clojure in environments where the JVM cannot run, notably Unity game engine (including iOS via IL2CPP).
MAGIC is a self-hosting compiler: it is written in Clojure and compiles itself through Nostrand, its task runner.
| Component | Description | Language |
|---|---|---|
| clojure-runtime | Persistent data structures, Keywords, Vars, reader | C# |
| magic-runtime | Fast dynamic call sites | C# |
| mage | Symbolic MSIL bytecode emitter | Clojure |
| magic-compiler | The compiler (s-expressions to MSIL) | Clojure |
| nostrand | Task runner, dependency manager, REPL | C# + Clojure |
| magic-unity | Unity integration package (IL2CPP support), default variant | C# |
| magic-unity-dual | Coexistence variant of magic-unity with the runtime excluded from the Editor (for projects that keep stock ClojureCLR). Generated by bb gen-unity-dual; see Unity integration. |
C# |
| magic-unity-smoke | Standalone Unity project that exercises MAGIC under IL2CPP. Regression suite for AOT-only bugs, runs by hand on the verified Unity version (2022.3.62f3). |
Clojure + C# |
Each component has its own README with detailed documentation.
clojure-runtime/ is forked from ClojureCLR (a .NET port of Clojure maintained by David Miller). Its full commit history is preserved here, so David Miller and other ClojureCLR contributors appear in the GitHub contributors view.
Runtimes (C#, ship as DLLs)
clojure-runtime Clojure data: PersistentMap, Vars, Keywords (forked ClojureCLR)
magic-runtime dynamic call sites for Clojure-on-CLR
Compiler (Clojure, AOT-compiles .clj -> .clj.dll)
mage symbolic MSIL emitter
magic-compiler Clojure compiler proper (uses mage)
Host (C#)
nostrand CLI task runner; links the runtimes, hosts the compiler
Packaging
magic-unity UPM package: bundles runtime + compiler DLLs for Unity
bootstrap is a build phase, not a component: compile magic-compiler with the previous nos, copy the resulting .clj.dlls back into nostrand/references/, rebuild nos against them. See Development.
Two shippable artifacts; pick the one(s) your project needs.
| Artifact | What it is | Built from | Ships via | Consumer pulls via |
|---|---|---|---|---|
nos CLI |
Build-time task runner that compiles Clojure to MSIL. Used by Unity projects (at build time, before opening Unity) and by non-Unity Clojure libs that want CLR test runs | nostrand/ + clojure-runtime/ + magic-runtime/ + magic-compiler/ |
GitHub Releases tarball, cut on every v* tag by .github/workflows/release.yml |
install/nos.sh (one-line curl; requires mono runtime, no .NET SDK) |
magic-unity UPM package |
Play-time Clojure runtime + IL2CPP build pre-processor. Loaded by Unity inside a Unity project; not a standalone artifact | magic-unity/ subdir of this repo (source + tracked playtime DLLs under magic-unity/Runtime/Infrastructure/Export/) |
This repo, pinned by git tag | Unity's Packages/manifest.json UPM git URL with ?path=magic-unity#<tag> |
You need three things: the nos CLI (build-time), the magic-unity UPM package (Unity loads it at play time), and a small dotnet.clj in your Unity project root that tells nos what to compile.
-
Install
nos. Requiresmonoruntime (macOS:brew install mono; Debian/Ubuntu:sudo apt-get install -y mono-runtime). No .NET SDK needed.# Latest, version resolved from main's version.edn: curl -fsSL https://raw.githubusercontent.com/flybot-sg/magic/main/install/nos.sh | sh # Or pin a specific release (tag from https://github.com/flybot-sg/magic/releases): curl -fsSL https://raw.githubusercontent.com/flybot-sg/magic/main/install/nos.sh | MAGIC_VERSION=<tag> sh
Defaults install to
$HOME/.local/nostrand/with the launcher symlinked to$HOME/.local/bin/nos. Override withINSTALL_DIR=/INSTALL_LINK=env vars if needed. -
Add the package, compile, open Unity. The Unity integration guide covers the
Packages/manifest.jsonpin (and which of the two variants to pick: default, or.dualfor projects that keep stock ClojureCLR in the Editor), thedeps.edn/dotnet.cljtemplates,nos dotnet/build, and IL2CPP.
Same install/nos.sh line, no Unity needed. Drop a deps.edn + dotnet.clj at your library root, then nos dotnet/run-tests. Tests execute under Mono via the nos you just installed. Porting a Clojure library to MAGIC walks through the whole workflow: reader conditionals, deps.edn, and the three dotnet.clj shapes (derive from aliases, exclude, or hardcode the namespace list).
Consumer install needs bash, curl, tar, and mono runtime. Contributing to MAGIC needs:
gitdotnet(v7+)mono(only needed at build time to host Nostrand; will go away once we drop net471)bb(optional, for the dev workflows in Development)
git clone https://github.com/flybot-sg/magic.git
cd magic
bb build # or: dotnet buildThe full build takes a few minutes on first run. Subsequent builds are incremental. Day-to-day workflows are in Development below.
This repo mixes C# (runtimes + host) and Clojure (compiler + stdlib). Different edits need different rebuilds; doing the wrong one wastes minutes per iteration. The bb task runner encodes which rebuild matches which edit.
| You changed | Run | Why |
|---|---|---|
clojure-runtime/**/*.cs |
bb dev-runtime |
.clj.dll references runtime DLLs by name; new bodies load without re-bootstrap |
magic-runtime/Magic.Runtime/*.cs |
bb dev-runtime |
Same |
*.mustache (callsite templates) |
bb dev-callsites |
Regenerates .g.cs first, then rebuilds runtime |
nostrand/*.cs (host code) |
bb build-runtime |
Rebuilds nostrand (and runtimes transitively via ProjectReference) |
magic-compiler/src/**/*.clj |
bb dev-compiler |
Compiler is bootstrapped; cold tests need re-bootstrap |
magic-compiler/src/stdlib/**/*.clj |
bb dev-compiler |
Same |
| Fresh checkout | bb build |
Full bootstrap |
For interactive iteration on compiler .clj files, prefer bb repl plus (require '... :reload) over re-running bb dev-compiler each time.
MAGIC is self-hosting: the compiler is Clojure code that emits CLR bytecode, and it needs a previous version of itself to compile. The repo ships pre-built .clj.dll files in nostrand/references/ for this reason. They ARE the compiler that compiles the new compiler.
Practical consequence: bb dev-runtime does not need a bootstrap. Already-compiled .clj.dll files reference Magic.Runtime.dll and Clojure.dll by name and pick up new bodies at load time. Only bb dev-compiler (changes to compiler or stdlib .clj source) triggers the slow re-bootstrap path.
Two folders of pre-built binaries are tracked in git:
nostrand/references/*.clj.dll: the compiler Nostrand loads at startupmagic-unity/Runtime/Infrastructure/Export/*.dll: the prebuilt runtime that Unity loads at play time
The bb dev-* tasks auto-revert any changes to these folders so day-to-day iteration commits stay clean. A maintainer refreshes them on purpose, usually after a batch of compiler or runtime fixes, by running either bb build (full path) or bb build-magic followed by bb build-bootstrap (faster: bootstrap + deploy). For stdlib-only edits (any magic-compiler/src/stdlib/**/*.clj), bb refresh-stdlib recompiles only the affected .clj.dll files and updates the SHA256 manifest (magic-compiler/stdlib-manifest.edn) that bb check-drift uses. Both paths refresh nostrand/references/ and magic-unity/Runtime/Infrastructure/Export/ together.
bb tasks # list all tasks with their docs
bb build # full build (minutes, first time)
bb build-runtime # incremental C# runtimes + nostrand (seconds)
bb test # run magic-compiler test suite
bb dev-runtime # build-runtime + test (after C# runtime edits)
bb dev-callsites # regen + build-runtime + test (after .mustache edits)
bb dev-compiler # build-magic + test (after compiler .clj edits)
bb check-drift # regen and fail if .g.cs files are stale (CI uses this)
bb repl # nostrand CLI REPL in magic-compiler/
bb clean # remove bin/ and bootstrap/Walk a form through the stages: form → macroexpand → AST → symbolic IL.
bb pipeline '(let [x 1] (+ x 1))'Prints to stdout:
| Section | What it shows |
|---|---|
FORM |
The input |
MACROEXPAND |
Shown only when expansion differs from input |
AST (skeleton) |
Analyzer output with bookkeeping keys (:env, source-position, :raw-forms) stripped |
TYPES |
Every AST node carrying type info; useful for diagnosing intrinsic rewrites, static vs dynamic call-site selection, and numeric promotion |
SYMBOLIC IL |
Flat instruction listing in emission order |
Also writes EDN dumps under magic-compiler/target/:
| File | What it is |
|---|---|
pipeline-ast.edn |
Full AST, pprinted |
pipeline-il.edn |
Flat instruction list, pprinted (usually what you want) |
pipeline-il-tree.edn |
Nested IL tree as mage emits it. The nil placeholders are structural alignment that mage filters at byte-emit time; the flat dump is the same instructions without the nesting. |
Forms that synthesize CLR types (fn, defn, deftype, defrecord) work too. No destination assembly is needed.
Options (:key value pairs after the form):
| Key | Default | Effect |
|---|---|---|
:out |
target |
Output directory for EDN dumps |
:sections |
all eight | Subset of #{:form :macroexpand :ast :types :il :ast-edn :il-edn :tree-edn} controlling what renders |
bb pipeline '(let [x 1] x)' :out /tmp/inspect
bb pipeline '(let [x 1] x)' :sections '#{:ast :il}' # stdout-only, no files
bb pipeline '(.Length "hi")' :sections '#{:il-edn}' :out /tmp # just dump the flat ILbb pipeline shows the compiled IL for a form but never runs it. To see what
a form actually returns at runtime, evaluate it against a warm MAGIC runtime
over a socket prepl (the MAGIC analogue of a Clojure nREPL). Complementary to
bb pipeline: pipeline answers "how does this compile", prepl answers "what
does this do".
Start the server in one shell (it blocks, serving connections):
bb prepl-server # listens on 127.0.0.1:5555 (override: bb prepl-server 5560)Send forms from another shell. Each prints the structured reply map:
bb prepl-eval '(+ 1 2)'
#=> {:tag :ret, :val "3", :ns "user", :ms 1.3, :form "(+ 1 2)"}
bb prepl-eval '(def answer 42)' # global defs persist across calls
bb prepl-eval '(* answer 2)' #=> {:tag :ret, :val "84", ...}
bb prepl-eval '(/ 1 0)' # errors come back as data, not a text dump
#=> {:tag :ret, :exception true, :val "{... :cause \"Divide by zero\" :phase :execution}", ...}Reply tags: :ret (eval result + :ms timing), :out/:err (captured
stdout/stderr during eval), :tap (values from tap>); an :exception true
:ret carries the Throwable->map data. The server is MAGIC Clojure
(clojure.core.server/io-prepl); the bb prepl-eval client is plain babashka,
so each call is fast. This runs only under Mono/nostrand: prepl evaluates at
runtime, so it has no IL2CPP/AOT path.
bb clean
bb build
bb check-drift # catches forgotten regen after .mustache edits
bb testThe actual build is orchestrated by Magic.csproj. The bb tasks call into these; invoke them directly if you'd rather not install bb.
| Target | Description |
|---|---|
dotnet build |
Full build (all components in dependency order) |
dotnet build -t:Nostrand |
Build task runner only |
dotnet build -t:Magic |
Magic compiler bootstrap (requires mono) |
dotnet build -t:Bootstrap |
Copy compiler DLLs into Nostrand, rebuild |
dotnet build -t:MagicUnity |
Unity package |
dotnet build -t:Clean |
Remove build artifacts |
bb test
# or directly:
cd magic-compiler && mono ../nostrand/bin/Release/net471/NostrandMain.exe test/allThe MAGIC compiler itself uses the test/all entrypoint in magic-compiler/test.clj. Downstream projects use their own nos dotnet/run-tests (see the Getting Started section above for the pattern).
For IL2CPP-specific regressions (AOT-only bugs that the Mono editor cannot catch), magic-unity-smoke drives MAGIC's compile output through Unity's IL2CPP pipeline and reports pass/fail in the built player. Run by hand on the verified Unity version after touching the compiler, the runtimes, or magic-unity itself.
This monorepo consolidates 6 repositories using git-filter-repo. All commits, authors, and dates are preserved. Scope history to any component:
git log -- nostrand/
git log -- magic-compiler/
git blame magic-compiler/src/magic/core.cljMAGIC was created and developed by Ramsey Nasser and contributors from 2014 to 2023.
This monorepo version is maintained by Flybot Pte. Ltd. from 2026.
- Most components: Apache License 2.0
clojure-runtime/: Eclipse Public License 1.0, derived from ClojureCLR by David Miller and contributors