From 553d76ade349a1fbba7e0d3e7b8ba50cd63059fd Mon Sep 17 00:00:00 2001
From: tamnd <1218621+tamnd@users.noreply.github.com>
Date: Sun, 6 Sep 2026 08:12:05 +0700
Subject: [PATCH] irxmanim: the core mobjects, and a scene that moves them
manim's vocabulary, none of manim. Mobjects, a scene, and `play` for one
beat of it, rendered as a single inline SVG with a stylesheet in it.
manim itself renders video, which wants cairo, ffmpeg and usually a
LaTeX install, and none of that fits in the first cell of a Colab
notebook or survives being saved into one.
The mobjects are Box, Code, Caption, Arrow, Group and the Wash a
highlight fades in. Code is a group with one mobject per line, so a pass
can be shown doing what a pass does, which is to touch one instruction
and leave the rest alone. The animations are fade_in, fade_out, move,
highlight, pulse and draw.
Three things this closes off deliberately:
CSS keyframes, no script. The output goes into a saved notebook, into
Colab's output sandbox and into static HTML, and script survives none of
those reliably. It also animates as an ordinary .
The markup is the last frame. Elements are written out where they end
up and the animation walks back to the start, so a renderer with no
stylesheet, a printed page, and anybody who asked for less motion get
the finished picture rather than a blank one. prefers-reduced-motion is
honoured. `frame(t)` gives any other moment as a still.
Easing is sampled here rather than handed to CSS, because a keyframe
timing function applies per stop and the stops of one mobject end up
interleaved with another's.
Ninety seconds is the cap from CONTRIBUTING.md, and a scene over it
raises rather than rendering.
Also: irx.ir grows `pieces()`, which is the tokeniser loop that both
irx.ir.highlight and irx.graphs already had a copy of, and now the three
things that render IR agree about what a keyword is.
docs/diagrams/pass-step.svg is the first scene, and the IR in it is what
`opt -passes=dce -S` actually printed.
---
docs/diagrams/pass-step.py | 75 ++++++
docs/diagrams/pass-step.svg | 1 +
toolkit/README.md | 36 +++
toolkit/irx/graphs.py | 18 +-
toolkit/irx/ir.py | 33 ++-
toolkit/irxmanim/__init__.py | 86 ++++++
toolkit/irxmanim/mobject.py | 414 ++++++++++++++++++++++++++++
toolkit/irxmanim/scene.py | 479 +++++++++++++++++++++++++++++++++
toolkit/pyproject.toml | 5 +-
toolkit/tests/test_irxmanim.py | 287 ++++++++++++++++++++
10 files changed, 1407 insertions(+), 27 deletions(-)
create mode 100644 docs/diagrams/pass-step.py
create mode 100644 docs/diagrams/pass-step.svg
create mode 100644 toolkit/irxmanim/__init__.py
create mode 100644 toolkit/irxmanim/mobject.py
create mode 100644 toolkit/irxmanim/scene.py
create mode 100644 toolkit/tests/test_irxmanim.py
diff --git a/docs/diagrams/pass-step.py b/docs/diagrams/pass-step.py
new file mode 100644
index 0000000..aceee10
--- /dev/null
+++ b/docs/diagrams/pass-step.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+"""What a pass does to a function, one instruction at a time.
+
+The IR in here is real. It went through `opt -passes=dce -S` from the pinned
+toolchain, and what comes out is the four lines the animation ends on. The
+transcript is in the comment below so that a reader can run it themselves,
+which is cheaper than trusting a picture.
+"""
+
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+sys.path.insert(0, str(ROOT / "toolkit"))
+
+from irxmanim import Box, Caption, Code, Scene, fade_in, fade_out, highlight, move # noqa: E402
+from irxmanim.mobject import LINE_H # noqa: E402
+
+OUT = Path(__file__).resolve().parent / "pass-step.svg"
+
+# $ opt -passes=dce -S <<'EOF'
+# define i32 @f(i32 %x) {
+# %a = add nsw i32 %x, 1
+# %b = mul nsw i32 %x, 7
+# ret i32 %a
+# }
+# EOF
+# define i32 @f(i32 %x) {
+# %a = add nsw i32 %x, 1
+# ret i32 %a
+# }
+BEFORE = [
+ "define i32 @f(i32 %x) {",
+ " %a = add nsw i32 %x, 1",
+ " %b = mul nsw i32 %x, 7",
+ " ret i32 %a",
+ "}",
+]
+DEAD = 2
+
+
+def build() -> Scene:
+ s = Scene(430, 208, caption="opt -passes=dce deletes the instruction nothing reads")
+
+ title = Caption(24, 20, text="opt -passes=dce", tone="loud")
+ frame = Box(20, 48, w=390, h=100)
+ code = Code(x=36, y=58, lines=BEFORE)
+ asked = Caption(24, 162, text="nothing reads %b, so nothing needs it")
+ done = Caption(24, 162, text="four instructions became three")
+ s.add(title, frame, code, asked, done)
+
+ s.play(fade_in(title), fade_in(frame))
+ s.play(fade_in(code))
+ s.play(highlight(code[DEAD]), fade_in(asked))
+ s.play(fade_out(code[DEAD]), run_time=0.4)
+ # The lines below the deleted one close up, which is the part of a pass
+ # that a printed before and after cannot show you.
+ s.play(
+ move(code[3], dy=-LINE_H),
+ move(code[4], dy=-LINE_H),
+ fade_out(asked),
+ run_time=0.5,
+ )
+ s.play(fade_in(done))
+ return s
+
+
+def main() -> None:
+ s = build()
+ OUT.write_text(s.svg() + "\n", encoding="utf-8")
+ print(f"{OUT.relative_to(ROOT)}: {s.duration:.1f}s, {len(s.script)} beats")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/diagrams/pass-step.svg b/docs/diagrams/pass-step.svg
new file mode 100644
index 0000000..fa36be3
--- /dev/null
+++ b/docs/diagrams/pass-step.svg
@@ -0,0 +1 @@
+
diff --git a/toolkit/README.md b/toolkit/README.md
index d8c8f7e..5e0ea58 100644
--- a/toolkit/README.md
+++ b/toolkit/README.md
@@ -251,6 +251,42 @@ Nothing here works out what the successors of a block are. That is the whole poi
**No Graphviz.** `dot` is not installed on every machine this has to run on, so the layout is here instead: rank each node one below its furthest predecessor, break the cycles first with a depth first walk, and route the two awkward cases, back edges and edges that skip a row, around the margin rather than straight through whatever box is in the way. The output is one inline `