The playground is MimIR in the browser: the mim CLI compiled to WebAssembly by Emscripten, plus a small page that feeds it a buffer and renders what comes back. Its sources live in web/; a build stages them next to the Emscripten artifacts in ${CMAKE_BINARY_DIR}/playground, so serving that directory over HTTP is all it takes to run it. A build of the current master is live at https://mimir.github.io/playground/.
| File | Role |
|---|---|
| index.html | The page: editor on the left, tabbed output on the right. |
| graph.html | The pop-out window: the graph, and nothing else. |
| playground.css | Styling, including the light/dark palette and the log's terminal colors. |
| playground.js | Editor, tabs, run loop, Graphviz layout. |
| graphview.js | Pan, zoom and highlight over a laid-out graph; the Graph tab and the pop-out share one. |
| popout.js | The pop-out's glue: it renders the SVG the page sends over. |
| mim-worker.js | Runs mim in a Worker, so a program that never terminates can be killed. |
| code.js | docs/code.js, staged; the lexer machinery the page and the docs share. |
| mim-code.js | docs/mim.js, staged; the Mim lexer, renamed out of Emscripten's mim.js way. |
| llvm-code.js | docs/llvm.js, staged; the LLVM lexer of the LLVM tab. |
| share-link.js | docs/share-link.js, staged; compresses a program into a #z= fragment and back. |
| darkmode-toggle.js | doxygen-awesome-darkmode-toggle.js, staged; the very toggle these docs use. |
| examples/*.mim | lit/docs/*.mim, staged; the examples are the ones the lit suite covers. |
| mim.{js,wasm,data} | Emscripten's output; mim.data carries the plugins' .mim halves and the ll runtime. |
Install the SDK once and put it in the environment of every shell that configures or builds:
A cross build cannot run the mim it just built, but bootstrapping a plugin needs one, so point MIM_NATIVE_MIM at a native binary — an ordinary build/bin/mim does. Bring it up to date first:
mim_playground also stages the page, so a change to it alone is a second's rebuild — no need to touch the compiler.
The page loads its Worker and wasm over fetch, which file:// refuses; serve the directory instead:
Then open http://localhost:8080. The server keeps running until you stop it with Ctrl-C; after a rebuild just reload the page.
The editor compiles on every keystroke, half a second after you stop typing; Compile turns into Stop while a compile is in flight and kills a program that does not terminate. optimize runs the opt pipeline and the ll backend; without it the program is only parsed and emitted again. The Mim tab's typed lets box is --mim-typed-let; see Command-Line Reference for what it does. ASCII is -a, which is a global flag rather than a Mim-only one: it swaps the UTF-8 spellings out of every output, the graph's labels included.
The vi box in the editor bar switches the editor to vi keybindings — @replit/codemirror-vim, fetched from a CDN the first time you tick it. The bar below the editor shows the mode and takes : commands; :w runs the program, which is the only thing there is to write to. The box is remembered across reloads, and it is gone altogether when the editor falls back to a plain textarea because CodeMirror did not load. Ctrl-W is not among the insert-mode keys that reach the editor: the browser reserves it for closing the tab, and no page can take it back.
A #z= URL fragment loads code instead of the first example, so a link can carry a whole program — this one increments an I32. docs/share-link.js produces it: UTF-8 bytes, deflated (CompressionStream('deflate-raw')), then base64url — ShareLink.encode in the browser's console produces exactly what the page expects. A fragment never leaves the browser (unlike a query string, which servers and proxies log), and compression keeps even a sizeable program well under the URL-length limits a ?src= link used to run into. The picker keeps such a program under (custom), so loading an example does not lose it. The buffer itself is kept in localStorage on every pause in typing and comes back under (custom) on the next visit, with a #z= link taking precedence; leaving the page with a buffer that is not an example asks for confirmation first. Every Mim snippet in these docs is a link of that kind: hover it and the ▶ button next to the copy button opens it here.
The Graph tab lays out --output-dot with Graphviz, and its checkboxes are exactly the CLI's --dot-* switches:
| Checkbox | Flag |
|---|---|
| default filter | --dot-default-filter |
| type edges | --dot-follow-types |
| inline consts | --dot-inline-consts |
| hidden edges | --dot-show-hidden |
| no tooltips | --dot-no-tooltip |
| lean labels | --dot-lean-labels |
See Command-Line Reference for what each one does. Since they are compile-time flags, toggling one re-runs the compiler.
Layout happens on the page, so a graph beyond 512 KB of DOT is refused rather than attempted. The last two checkboxes are the ones that buy room — on an optimized ackermann.mim the tooltips are two fifths of the file and the labels another third, and the two together take it from 30 KB to 7 KB. They are what a refused graph needs; the price is the hover text and the ports an edge docks at.
Drag to pan and turn the wheel to zoom at the pointer; Fit, a double-click, or 0 frames the whole graph again, and +/- zoom from the keyboard. A re-run keeps the current view: the editor recompiles on every pause in typing, so re-framing each time would leave no way to stay zoomed in on one spot.
Hovering a node dims everything but that node, its direct neighbours and the edges between them; a click pins that focus, and a click elsewhere or Esc releases it. The highlight paints detached edges too, so it shows per node what hidden edges shows for the whole graph — without a recompile.
The button left of the zoom controls opens the graph in a window of its own, with the same controls and nothing else. The page lays the DOT out and sends the SVG over, so the pop-out needs neither Graphviz nor the compiler and follows every re-run — including while the Graph tab itself is down, which leaves the whole page for the editor and the other tabs.
The toggle in the header is the doxygen-awesome one these docs use, staged from the submodule, so both offer the same control and — served from one origin — share the stored preference. The graph's colors are baked by the compiler rather than applied by CSS, so flipping the theme re-runs it with --dot-dark, which drops the white backdrop and lightens the edges.
.github/workflows/playground.yml does all of the above on every push, compiles a program with the binary it just built — a wasm-only regression such as the one behind Def::ops_ptr fails there instead of silently shipping — and uploads build-wasm/playground/ as an artifact.
A push to master also deploys that directory into playground/ of the site repository, next to the Doxygen output, via docs/deploy.py --subdir playground. The docs deploy sweeps the site root, so playground/ only survives by being listed in that script's FOREIGN_DIRS. Both workflows push to the same repository from their own concurrency group and rely on the script's rebase retry; they never write the same paths.