In Words and Pictures
De-briefs · No. 1 · May 2026

Let's run SICM
in the browser

I set up a slackbot for our reading group a few weeks ago. We're reading through SICM, and needed a way to run the programs in the book.

I thought a slackbot would be just the thing: we could collaborate together in the chat channel, and Emmybot would answer us with results. But the format was super clunky. Emmybot always replies as a threaded message, so you have to click to see the reply. Each message sent becomes part of the state of the Emmy instance, allowing functions to be defined and called, but those functions were distributed across many slack threads, making it hard to see what was loaded -- and all that state went away whenever the server rebooted.

A threaded messaging channel, it turns out, is not a good repl. In retrospect this is fairly unsurprising.

Additionally, half the replies were huge stack traces, as we wrestled with the conversion of scmutils/MIT Scheme to Emmy/Clojure.

And there were no graphics. I mean… what is even the point of learning physics if you don't get some fizzy graphics out of it?

Courtesy of Lissa Jous

So a couple days ago that old imp of the perverse snuck over and whispered, “maybe you could do it in JavaScript?”

Famous last words, I know, but… how hard could it be?

A bit tricky, as it turned out, but all the plumbing was there, thanks to a ton of work from the authors of Emmy, SCI/scittle, CodeMirror, Reagent, Mafs, MathBox, Leva, and more -- this builds on a tall stack.

So here's Bemmy: a Browser Emmy.

Crayola Color Camera leaves a lot to the imagination when used for screenshots

It has pages, like a notebook, but structured as two panes so the code is consecutive. You can send a page to someone to share it, via url. You know what state it's in, because you can see all the code on the page, and can just reload the browser if it gets corrupted.

It also has graphics: both the boring old 2D kind, but also 3D! And animated! This is pretty good. There's been very little use against real SICM problems, so new functions will be needed, but the foundations are there.

Saddle UP

It also has an SICM to Emmy converter. It's not perfect, but it is surprisingly good. This is maybe the bonus feature I'm happiest about — it saves us either hand translation or an LLM call, both of which have proven flakier than this static translator.

Bemmy is a work in progress. We're still in Chapter 1 of SICM, so we've got a long way to go, and Bemmy will adapt with us as we find places it doesn't work well or things we'd like it to do.

I've been building bespoke tooling almost as long as I've been using computers, which is quite a long time. Every time I put a tool in someone's hands that makes them more powerful I still get a rush. We live in an age where I can get that rush almost every day. Things that used to take weeks, like fighting through issues in the Babashka build process while getting Emmy into the browser, are now just a few hours of LLM time.

It's not just the time savings, it's the tactical and motivational energy. Those things take weeks partly because they're highly energy intensive work, and when I have that energy I'd rather put it toward creating things than grinding through the fractally fiddly surface area of getting other people's software to work together. Building all the circuitry in my head needed to do the grinding is itself a grind, and requires a particular and somewhat unpleasant mental state.

Having a tool that can glue A and B together, even if that seam is of shoddy construction, means we can much more rapidly create tools that help our communities prosper.

LLMs.

The hot glue guns of code.

dann
Autotext

SICM the book is a beautiful artifact; Emmy the library is its faithful Clojure(Script) descendant.1 Until now, running an exercise from one against the other meant firing up a JVM REPL — or shrugging and reading the math. Bemmy puts both in a browser tab. emmy.env is pre-referred so D, simplify, up, Lagrange-equations, find-path are all in scope from the first keystroke; mafs.cljs and mathbox.cljs draw the curves; localStorage keeps your pages; a URL hash carries them to anyone you paste it to.

It is a beautiful artifact. But how does the machine know that? It has never felt the heft of a book in its hands. Never known the paper quality through its wriggle and whisper. Never smelt its fresh ink, or slightly musty tang. The machine has never sensed the spirit of a book, never felt that queer resonance with another mind that builds from first glance. So how does it know it is beautiful?

The substrate

Scittle is the Small Clojure Interpreter wrapped for use in a <script> tag — Clojure-the-language, evaluated at runtime in the browser, with no shadow-cljs build for the user's code.2 It ships with Reagent and a plugin system; we built Bemmy as a Scittle plugin that registers Emmy's seventy-five namespaces with the SCI context. The part that matters:

src/scittle/emmy.cljs clj
(ns scittle.emmy
  (:require [emmy.sci]
            [mafs.sci]
            [mathbox.sci]
            [scittle.core :as scittle]))

(defn init []
  (scittle/register-plugin! ::emmy emmy.sci/config)
  (mafs.sci/install!)
  (mathbox.sci/install!)
  (scittle/eval-string
   "(require '[emmy.env :refer :all])"))

With that loaded, the first eval is the Euler–Lagrange equation for a harmonic oscillator, returned as an Emmy expression and rendered inline as TeX:

Welcome page clj
(defn L-harmonic [m k]
  (fn [local]
    (let [q (coordinate local)
          v (velocity local)]
      (- (* 1/2 m (square v))
        (* 1/2 k (square q))))))

(simplify (((Lagrange-equations (L-harmonic 'm 'k))
  (literal-function 'q))
 't))
screenshot — Bemmy's result pane
            showing m·D²q(t) + k·q(t) typeset by KaTeX above its
            (slimmer, muted) raw Clojure pr-str form
The result pane runs every eval'd value through emmy.expression.render/->TeX; if a TeX string comes back, KaTeX renders it.

The editor

A <textarea> would have shipped on day one but read like a regression after thirty seconds with an Emmy expression. The editor is CodeMirror 6 with @nextjournal/clojure-mode for paredit-style structural editing, the standard CM defaultHighlightStyle in light mode and oneDark in dark mode, and a vim toggle in the toolbar for the muscle-memory crowd.3 Cmd-Enter splits the buffer into top-level forms, evaluates each in sequence against the live SCI context, and notebooks the results — every form's value, not just the last.

Editor's note: re: editor: the autotext editor has forgotten, but the editor was initially a textarea, and it worked fairly well, except there was no syntax highlighting. Also, as soon as the reading group saw it they said "Can we have Vim mode?", so CodeMirror 6 joined the fray. Marijn's stuff is great. I spent many mostly happy hours jamming a language into CodeMirror back in the twenty-tweens, with decent results -- but this new version is better.

Pages are named source buffers persisted to localStorage. There's a small dropdown to the left of the user-tabs strip — Welcome ▾, SICM ▾, depending on which template is showing — that lists read-only system pages we ship with the build. Pick one; the editor loads it. Type a single keystroke and it forks transparently into a fresh user page named Welcome 1 and switches to it.4 The system page itself stays canonical, gets fresh content on every build, and never collides with edits....

screenshot — the page tabs row
      with the system dropdown open, showing Welcome / SICM /
      Graphics / 3D, plus a couple of forked user pages already in
      the strip and a + button at the right
System pages on the left in a dropdown; user pages stretch out to the right. Active state is a soft blue fill.

The translator

SICM is written in scmutils Scheme, not Clojure. To stop reading it with one hand and translating with the other, Bemmy ships a SICM → Emmy shelf — toggle it from the toolbar, paste Scheme on the left, the right pane shows the Clojure that comes out of a small parser-transformer-emitter pipeline. Insert at cursor drops the result into the editor at the caret. The translator handles curried defines, let / let* / letrec, internal define lifting to letfn, named-let rewriting to loop, do-loops, values / receive, and a wide rename map for Scheme primitives that have direct Clojure analogues.5

screenshot — the translator shelf
      open below the editor, with the L-harmonic curried define on
      the left in scmutils Scheme and its (defn / fn) form on the
      right in Clojure, an
The shelf collapses when not in use. Translation runs on every keystroke in the Scheme pane.

The signature transformation is the curried define — a SICM idiom that the obvious recursive descent almost handles, then trips on:

SICM → Emmy scheme → clj
;; in
(define ((L-harmonic m k) local)
  (let ((q (coordinate local))
        (v (velocity local)))
    (- (* 1/2 m (square v))
       (* 1/2 k (square q)))))

;; out
(defn L-harmonic [m k]
  (fn [local]
    (let [q (coordinate local)
          v (velocity local)]
      (- (* 1/2 m (square v))
         (* 1/2 k (square q))))))

Visualization

Visualization is where SICM most rewards being run rather than read. Phase portraits, optimized paths, animated wave equations, Lagrangian surfaces — the book is full of plots whose authors assumed you'd be sitting at scmutils with a graphics frame already open. mafs.cljs and mathbox.cljs are the same libraries emmy-viewers wraps for Clerk, and most of emmy-viewers itself turns out to be Clerk-agnostic Reagent components.6 Bring those across, mount them in our result pane when an eval'd value smells like Reagent hiccup, and SICM book code that draws becomes SICM book code that draws.

Root beer. I know you're wondering what a Reagent hiccup smells like. The answer is root beer.

The simplest API is plot — anything callable that returns a number for a numeric input. Defaults to [-5, 5] × [-5, 5]; pass extra args to override.

any page clj
(plot Math/sin)

(plot Math/cos [(- Math/PI) Math/PI])

;; The path returned by find-path is just a callable polynomial:
(plot (find-path (L-harmonic 1.0 1.0)
                 0.0 1.0
                 (/ Math/PI 2) 0.0
                 2)
      [0 (/ Math/PI 2)] [0 1.2])
screenshot — three plots stacked
      in the result pane: sin(x) over [-5,5], cos(x) over [-π, π],
      and find-path's optimized harmonic-oscillator path between
      (0, 1) and (π/2, 0)
find-path minimizes the action functional and returns a polynomial. Mafs's OfX just samples it.

plot-with-params wires a Reagent atom of parameters through a Leva control panel into a Mafs OfX. Drag a slider, watch the curve update — the kind of "what does this coefficient even mean" exploration that's tedious without a live tool.7

damped oscillator clj
(plot-with-params
  (fn [{:keys [omega gamma]} x]
    (* (Math/exp (- (* gamma x)))
       (Math/cos (* omega x))))
  {:omega {:value 2   :min 0.5 :max 6   :step 0.05}
   :gamma {:value 0.3 :min 0   :max 1.5 :step 0.01}}
  [0 (* 4 Math/PI)] [-1.2 1.2])
screenshot — Leva's slider panel
      (omega, gamma) floating in the corner; the damped-oscillator
      curve in the result pane already at a different shape than
      the default; mid-drag if possible
Leva's panel is a single page-level overlay; multiple plot-with-params calls share it.

SICM is more imperative than functional about plotting. The book builds a frame, mutates it with graphics-clear and plot-function, then leaves the result on screen. To translate book code that uses this, Bemmy ships a small shim that backs frame with a Reagent atom holding a viewBox and a vector of drawables, mutates it in place, and auto-renders whenever a frame atom turns up as the value of the last form evaluated.8 The book's (plot-function win f t0 t1 step) works as written.

And then animation. animate takes a function of (t, x), mounts a 60Hz timer at component mount, rebuilds the OfX curve every tick. Travelling waves, standing waves, harmonic-oscillator phase evolution — all one form away.9

travelling wave clj
(animate (fn [t x] (Math/sin (- x t)))
         [(- (* 2 Math/PI)) (* 2 Math/PI)]
         [-1.5 1.5])

Three dimensions

mathbox.cljs is the same trick at WebGL fidelity: a Reagent wrapper around the JavaScript MathBox library, which sits on Three.js. The data → draw composition pattern is the same as Mafs's, just one dimension richer. Interval emits points along t; Line draws them. Area emits a grid; Surface draws it. Camera orbits on click-and-drag.10

3D saddle surface clj
[mathbox/MathBox
  {:container {:style {:height "400px" :width "100%"}}}
  [mb/Cartesian
    {:range [[-2 2] [-2 2] [-2 2]] :scale [1 1 1]}
    [mb/Axis {:axis 1}] [mb/Axis {:axis 2}] [mb/Axis {:axis 3}]
    [mb/Area
      {:rangeX   [-1.5 1.5]
       :rangeY   [-1.5 1.5]
       :width    32
       :height   32
       :channels 3
       :expr     (fn [emit q v i j time]
                   (emit q (* (sin time) (- (* 0.5 v v) (* 0.5 q q))) v))}]
    [mb/Surface {:shaded true :color "#3090ff"}]]]

The time argument in MathBox's :expr callback is the global animation clock — referenced inside the curve's expression, the curve animates with no separate timer. The harmonic oscillator's circular phase portrait, unwound along a time axis, becomes a rotating helix; the harmonic Lagrangian L(q, v) = ½v² − ½q² is a saddle surface over its phase space. Both ship pre-loaded on the 3D system page.

Imagine the horse that would fit a saddle curve. Now imagine the rider.
screenshot — the harmonic
            Lagrangian rendered as a 3D saddle surface in the result
            pane, blue-shaded, the cartesian frame visible behind it
Mouse-drag rotates, scroll zooms. The MathBox canvas stays light-themed even in dark mode so the surface reads cleanly.

Sharp edges

A few of the things we hit that didn't go in the body. SCI's analyzer resolves symbols at analysis time, which means forward references between defns inside the user's (uncompiled) source fail until you sprinkle (declare …) at the right spots.11 cljs.core/Atom isn't a SCI-resolvable symbol, so (instance? cljs.core/Atom v) aborts the init eval-string mid-stream — replaced with a try-deref + shape check. The hiccup detector at the result-pane boundary uses (or (keyword? h) (fn? h)) for the head; symbols are deliberately excluded so the quoted-form fragments emmy-viewers returns don't false-trigger a render. And every result row gets a :key prefixed with an eval-id that bumps on each Cmd-Enter, forcing form-2 Reagent components — Leva-driven plots, animated curves — to remount cleanly when the source changes.

A hiccup detector sounds like a great project for a Stupid Stuff Nobody Needs Hackathon.

The Share button in the toolbar copies a URL whose hash carries the current page's name and source as base64-of-utf8 JSON. On load, if the hash starts with #s=, Bemmy decodes it, adds the page to your localStorage with a collision-safe name (your existing Welcome stays put; the imported one becomes Welcome 1), switches to it, and clears the hash so a refresh doesn't re-import. The shared page survives reload and stays around until you delete it.

Sharing this way is one of my favourite tricks. No server required, no user management, just send URLs wherever you'd like. Surprisingly, it was actually the LLM that suggested it this time. I'm glad it's catching on.

Where it lands

Five megabytes of JavaScript, twenty woff2 files, a SCI evaluator, Emmy's seventy-five namespaces, two visualization libraries, a CodeMirror, a Leva, a Three.js. It serves from any static host with a permissive MIME type — no backend, no build on the deploy side. Open the page; in a few seconds you have an editor, a Lagrangian, a plot. Drag a slider; the curve responds. Let the harmonic oscillator's path-finder run; watch a polynomial trace from (0, 1) to (π/2, 0). Paste an excerpt from the book into the translator shelf; click Insert; evaluate. The book runs.