Check out the demo plugin for a minimal example. It uses our custom add_mim_plugin CMake command. A plugin generally consists of two halves with the same name: a <plugin>.mim file that declares the public annexes and a shared library that registers the runtime behavior.
Plugin names may only contain letters, digits, and underscores, and are limited to 8 characters.
The demo plugin at src/mim/plug/demo/ is the minimal, complete example every in-tree plugin follows. It consists of five checked-in files:
CMakeLists.txt:
demo.mim:
Doc comments (///) are ordinary Doxygen-flavored Markdown (headings, [TOC], @see, ...); everything else is plain Mim syntax declaring the annex itself. Here demo.const_idx is a single axiom with no subtags, and normalize_const names the C++ function that evaluates it. Building the plugin auto-generates a C++ header and a Python module from this very file, and turns its doc comments into the plugin's Doxygen page — see Generated Interfaces below, since this applies to every plugin, not just demo.
It is a header, not a translation unit of its own: demo.cpp and normalizers.cpp #include it, and it in turn #includes the generated header it wraps. See Generated Header below for why it is just this two-line indirection.
The function mim_get_plugin is the single entry point Driver looks up (via dlopen/dlsym) when a plugin demo; directive or -p demo loads the shared module. It returns a mim::Plugin record: the plugin's name, the MIM_VERSION it was built against (checked against the loading mim binary), the register_normalizers function that normalizers.cpp defined via MIM_demo_NORMALIZER_IMPL, and an optional backend-registration callback (nullptr here, since demo provides no backend).
normalizers.cpp:
Normalizers usually obtain the owning World from one of their arguments — here type->world() — and build the replacement directly in that world, without ever materializing the App node it replaces; that is why normalize_const just returns the literal 42. If a normalizer cannot do anything meaningful, it should return {}/nullptr: World::app treats a null return as "give up" and falls back to constructing the real App node from the original arguments instead. The macro MIM_demo_NORMALIZER_IMPL stems from the generated header (see below) that expands into a full definition of register_normalizers, wiring normalize_const up under the axiom's mangled id.
Every plugin's <plugin>.mim file is also machine-readable input to mim itself. MimIR's custom CMake command add_mim_plugin runs the freshly built mim binary over it once, in --bootstrap mode (which makes plugin directives behave as plain imports instead of dlopening other plugins that may not exist yet at header-generation time), asking for three outputs in one invocation:
This happens automatically as part of the normal build — plugin authors never invoke this by hand. See the CLI reference for the flags themselves.
The switch --output-h walks the plugin's annexes after name binding and writes include/mim/plug/<plugin>/autogen.h into the build tree. For demo, that's:
For each annex tag it emits an enum class <tag> : flags_t (populated with subtags/aliases when the axiom has any — empty here, since const_idx has none), a forward declaration of the tag's normalizer matching mim::NormalizeFn's (type, callee, arg) -> const Def* signature, and — once per plugin — a register_normalizers declaration plus the MIM_<plugin>_NORMALIZER_IMPL macro that defines it, wiring every axiom's normalizer into the Normalizers map under its mangled Annex::Base<Tag> id. The Annex::Base/Annex::Num template specializations outside the plugin's namespace are implementation plumbing and are hidden from Doxygen via #ifndef DOXYGEN.
This generated header is never checked in and never included directly. Instead, each plugin checks in a thin wrapper at include/mim/plug/<plugin>/<plugin>.h that plugin sources #include — demo.h, shown above, is exactly that wrapper.
Larger plugins (e.g. core, mem, clos) follow the same wrapper pattern but add hand-written declarations around the #include, so plugin authors can extend the generated boilerplate without editing generated code. add_mim_plugin's INSTALL option installs autogen.h itself (not the wrapper) into <prefix>/include/mim/plug/<plugin>/ for out-of-tree consumers.
The switch --output-py emits the same information as an IntEnum, for tooling written in Python:
A tag with subtags gets its own _<plugin>_<tag>(IntEnum) class (later aliased as <plugin>.<tag>) instead of a plain member. This is the mim.plug.<plugin> module the Python bindings re-export; see Loading Plugins in the Python Bindings guide for world.annex(...)/world.call(...) from Python.
The switch --output-md works differently from -h/-py: rather than being derived from the bound AST, it is streamed straight out of the lexer while it scans <plugin>.mim. Text inside /// doc comments (ordinary Markdown, including Doxygen commands like [TOC] and @see) is copied through verbatim; every stretch of actual Mim syntax between comments is wrapped in a fenced `` code block. The result for demo.mim is written to docs/plug/demo.md` in the build tree:
docs/Doxyfile.in's INPUT includes that build-tree docs/plug directory, so Doxygen picks up every plugin's generated .md file directly as a documentation page, anchored at whatever {#...} id the plugin's leading doc comment declares ({#demo} here — the same anchor used by @ref demo elsewhere in the docs). add_mim_plugin also adds a sidebar tab (of type user, pointing at @ref <plugin>) to the Doxygen layout for every registered plugin, so this page is linked from the sidebar automatically. Writing good /// doc comments in a plugin's .mim file is therefore both the axiom documentation and the plugin's Doxygen page — there is no separate place to describe a plugin's annexes.
The MimIR Plugin Registry is the central hub for discovering, sharing, and maintaining third-party MimIR plugins. The registry lists available plugins and provides guidance on how to discover and use them. If you've created a plugin you'd like to share with the community, please consider submitting it to the registry.
Create a new in-tree plugin foobar based on the demo plugin:
The script also supports -h/--help and prints the same usage text when called incorrectly.
By default, the script creates an in-tree plugin and updates src/mim/plug/CMakeLists.txt. The generated files are:
To create a self-contained third-party plugin repository in extra/, use:
This creates extra/<plugin>/ with:
In --extra mode, the script also:
If you clone a plugin repository into extra/, MimIR picks it up automatically during configuration when the repository contains a CMakeLists.txt as a direct child of extra/.
If the plugin repository also contains lit/*.mim tests, they are picked up automatically by the main lit target as well.
To move an existing in-tree plugin into extra/foobar, use:
This moves:
It also:
The extracted plugin is staged with git add but not committed, allowing you to review the changes before committing.
After installing MimIR, a third-party plugin only needs to find the mim package. For example, a plugin called foo can be set up like this:
Configure the project standalone with:
The authoritative reference for add_mim_plugin itself lives in cmake/Mim.cmake.
Backends such as ll sometimes need to emit calls to functionality that is awkward or brittle to express as hand-written LLVM IR — for example libc helpers, or complex argument setup for vendor APIs. Instead of emitting the implementation inline, a backend can offload it to a small C wrapper that is compiled to LLVM IR by clang and pulled into the output. This keeps the backend focused on emitting LLVM and lets clang deal with platform- and version-specific details.
The add_mim_runtime CMake command compiles a plugin's C wrapper sources to textual LLVM IR:
All sources are merged into a single module <libdir>/mim/rt/<plugin>_rt.ll (next to the plugins) and, with INSTALL, installed alongside them — one runtime module per plugin, addressable by a well-known name no matter how many .c files it is split into. This step is optional: it requires clang (discovered as MIM_CLANG; merging multiple sources additionally needs llvm-link) and is skipped when clang is unavailable or MIM_BUILD_LL_RUNTIME is OFF.
The ll backend locates such a runtime module via the driver's search paths and either embeds it into or links it with its emitted module, selected via -X ll:rt=embed (default) or -X ll:rt=extern; see the CLI reference. The in-tree examples are src/mim/plug/ll/rt/mim_rt.c, which provides @mim_jmpbuf_size for %clos.alloc_jmpbuf, and src/mim/plug/ll_nvptx/rt/mim_cuda_rt.c, whose @mim_cu_check performs the ll_nvptx backend's CUDA driver-API error handling. The ll_nvptx backend reuses the very same load_rt_module helper as ll, differing only in the runtime module it names.
The authoritative reference for add_mim_runtime lives in cmake/Mim.cmake.