MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
Plugins

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.

Example: demo Plugin

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 — a single add_mim_plugin call
  • demo.mim — the public annex surface
  • demo.h — the public header, over in include/mim/plug/demo/
  • demo.cpp — the plugin's entry point
  • normalizers.cpp — the normalizer implementations

CMakeLists.txt:

add_mim_plugin(demo
SOURCES
demo.cpp
normalizers.cpp
INSTALL
)

demo.mim:

/// # The demo Plugin {#demo}
///
/// @see mim::plug::demo
///
/// A minimal demo plugin.
///
/// [TOC]
///
/// ## Operations
///
/// ### %%demo.const_idx
///
/// The constant `42`, evaluated by a normalizer.
///
axm %demo.const_idx: [n: Nat] → Idx n, normalize_const;

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.

demo.h:

#pragma once

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.

demo.cpp:

#include <mim/phase.h>
#include <mim/plugin.h>
using namespace mim;
/// Registers normalizers as well as Phase%s and Pass%es for the Axm%s of this Plugin.
return {"demo", MIM_VERSION, plug::demo::register_normalizers, nullptr};
}
Plugin mim_get_plugin()
Definition affine.cpp:17
#define MIM_EXPORT
Definition config.h:20
void register_normalizers(Normalizers &normalizers)
Definition ast.h:14
#define MIM_VERSION
Definition plugin.h:54
Basic info and registration function pointer to be returned from a specific plugin.
Definition plugin.h:59

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:

#include <mim/world.h>
namespace mim::plug::demo {
const Def* normalize_const(const Def* type, const Def*, const Def* arg) {
auto& world = type->world();
return world.lit(world.type_idx(arg), 42);
}
} // namespace mim::plug::demo
Base class for all Defs.
Definition def.h:265
#define MIM_demo_NORMALIZER_IMPL
Definition autogen.h:22
The demo Plugin
const Def * normalize_const(const Def *type, const Def *, const Def *arg)

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.

Generated Interfaces

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:

mim demo.mim --bootstrap \
--output-h build/include/mim/plug/demo/autogen.h \
--output-py build/lib/mim/demo.py \
--output-md build/docs/plug/demo.md

This happens automatically as part of the normal build — plugin authors never invoke this by hand. See the CLI reference for the flags themselves.

Generated Header

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:

#pragma once
#include <mim/axm.h>
#include <mim/plugin.h>
/// @namespace mim::plug::demo @ref demo
namespace mim {
namespace plug::demo {
static constexpr plugin_t Plugin_Id = 0x1463900000000000;
/// @name %%demo.const_idx
///@{
enum class const_idx : flags_t {
};
const Def* normalize_const(const Def*, const Def*, const Def*);
///@}
void register_normalizers(Normalizers& normalizers);
#define MIM_demo_NORMALIZER_IMPL \
void register_normalizers(Normalizers& normalizers) {\
normalizers[flags_t(Annex::Base<const_idx>)] = &normalize_const; \
}
} // namespace plug::demo
#ifndef DOXYGEN // don't include in Doxygen documentation
template<> constexpr flags_t Annex::Base<plug::demo::const_idx> = 0x1463900000000000;
template<> constexpr size_t Annex::Num<plug::demo::const_idx> = 0;
#endif
} // namespace mim
#ifndef DOXYGEN // don't include in Doxygen documentation
#endif
static constexpr plugin_t Plugin_Id
Definition autogen.h:10
u64 flags_t
Definition types.h:39
absl::flat_hash_map< flags_t, NormalizeFn > Normalizers
Definition plugin.h:22
u64 plugin_t
Definition types.h:40
static constexpr size_t Num
Number of Axm::subtags.
Definition plugin.h:147
static constexpr flags_t Base
Definition plugin.h:148

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 #includedemo.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.

Generated Python Module

The switch --output-py emits the same information as an IntEnum, for tooling written in Python:

from enum import IntEnum
class demo(IntEnum):
ID = 0x1463900000000000
const_idx = 0x1463900000000000

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.

Generated Markdown Page

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:

# The demo Plugin {#demo}
@see mim::plug::demo
A minimal demo plugin.
[TOC]
## Operations
### %%demo.const_idx
The constant `42`, evaluated by a normalizer.
```
axm %demo.const_idx: [n: Nat] → Idx n, normalize_const;
```

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.

Plugin Registry

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

Create a new in-tree plugin foobar based on the demo plugin:

./scripts/new_plugin.py foobar

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:

  • src/mim/plug/<plugin>/<plugin>.mim
  • src/mim/plug/<plugin>/CMakeLists.txt
  • src/mim/plug/<plugin>/<plugin>.cpp
  • src/mim/plug/<plugin>/normalizers.cpp
  • include/mim/plug/<plugin>/<plugin>.h
  • lit/<plugin>/const.mim

Create a Third-Party Plugin

To create a self-contained third-party plugin repository in extra/, use:

./scripts/new_plugin.py foobar --extra

This creates extra/<plugin>/ with:

  • <plugin>.mim
  • CMakeLists.txt
  • src/<plugin>.cpp
  • src/normalizers.cpp
  • include/mim/plug/<plugin>/<plugin>.h
  • lit/const.mim
  • .github/workflows/{linux,macos,windows}.yml — GitHub Actions CI/CD workflows

In --extra mode, the script also:

  • Initializes a new Git repository for the plugin
  • Generates GitHub Actions workflows that automatically build and test the plugin against the main MimIR repository
  • Patches workflow configurations to clone the main repository as the parent and the plugin as a submodule in extra/<plugin>

Third-Party Plugin Discovery

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.

Extract an Existing In-Tree Plugin

To move an existing in-tree plugin into extra/foobar, use:

./scripts/extract_plugin.py foobar

This moves:

  • src/mim/plug/<plugin>/ into extra/<plugin>/
  • include/mim/plug/<plugin>/ into extra/<plugin>/include/...
  • lit/<plugin>/ into extra/<plugin>/lit/

It also:

  • Rewrites the extracted CMakeLists.txt for out-of-tree use
  • Removes the plugin from the in-tree plugin list so it is picked up through extra/ instead
  • Generates GitHub Actions workflows that automatically build and test the extracted plugin against the main MimIR repository

The extracted plugin is staged with git add but not committed, allowing you to review the changes before committing.

Standalone Third-Party Builds

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:

cmake_minimum_required(VERSION 3.25 FATAL_ERROR)
project(foo)
if(NOT COMMAND add_mim_plugin)
find_package(mim REQUIRED)
endif()
add_mim_plugin(foo
SOURCES
src/foo.cpp
src/normalizers.cpp
)

Configure the project standalone with:

cmake .. -Dmim_DIR=<MIM_INSTALL_PREFIX>/lib/cmake/mim

The authoritative reference for add_mim_plugin itself lives in cmake/Mim.cmake.

Runtime Wrappers

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:

add_mim_plugin(foo
SOURCES
src/foo.cpp
)
add_mim_runtime(foo
SOURCES
rt/foo_rt.c
INSTALL
)

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.