Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs/diagrams/pass-step.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions docs/diagrams/pass-step.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions toolkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<svg>` with no script in it, which survives being saved into a notebook and served as static HTML.

## Making it move

`irxmanim` is the second package in this directory. It borrows manim's vocabulary, mobjects and a scene and `play` for one beat of it, and none of its implementation: manim 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.

```python
from irxmanim import Scene, Box, Code, Caption, fade_in, fade_out, highlight, move

s = Scene(430, 208, caption="opt -passes=dce deletes the instruction nothing reads")
code = s.add(Code(x=36, y=58, lines=before))
s.play(fade_in(code))
s.play(highlight(code[2]))
s.play(fade_out(code[2]))
s.play(move(code[3], dy=-17), move(code[4], dy=-17))
```

![A pass deleting the one instruction nothing reads, and the lines below it closing up](../docs/diagrams/pass-step.svg)

The mobjects are `Box`, `Code`, which is a group with one mobject per line so a pass can be shown touching one instruction and leaving the rest alone, `Caption`, `Arrow` and `link` for an arrow between two things, and `Group`. The animations are `fade_in`, `fade_out`, `move`, `highlight`, `pulse` and `draw`, which strokes an arrow from its tail to its head.

Print a scene and you get the storyboard, which is the form a review reads:

```
Scene 430x208, 4.6s, looping, 16 mobjects

0.0s fade in opt -passes=dce
1.0s highlight %b = mul nsw i32 %x, 7
2.0s fade out %b = mul nsw i32 %x, 7
2.4s move ret i32 %a
```

**CSS keyframes, no script.** This ends up in a saved notebook, in Colab's output sandbox and in static HTML on the site, and script survives none of those reliably. It also means the picture animates when it is loaded as an ordinary `<img>`, which script would not.

**The markup is the last frame.** Every element is written out where it ends up, and the animation walks back to the start and forward again. A renderer that drops the stylesheet, a printed page and anybody whose system asks for less motion all get the finished picture rather than an empty box. `scene.frame(t)` gives any other moment as a still, with nothing moving in it.

**Ninety seconds is the cap**, the one in `CONTRIBUTING.md`, and a scene that goes over it raises rather than rendering. A longer animation is two animations.

## When a tool fails

The default subprocess failure is `CalledProcessError: returned non-zero exit status 1`, which tells a reader who has never run `opt` before absolutely nothing. `irx` raises `ToolError` instead, with the command, the real stderr, and where possible a sentence about what to do:
Expand Down
18 changes: 2 additions & 16 deletions toolkit/irx/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from dataclasses import dataclass, field
from pathlib import Path

from .ir import COLOUR, KEYWORDS, TOKEN, Module
from .ir import COLOUR, Module, pieces
from .proc import run

# The pass name, and the shape of the file it leaves behind. `opt` prints
Expand Down Expand Up @@ -445,22 +445,8 @@ def _boxes(nodes: list[Node], rank: dict[str, int]) -> list[Box]:

def _tspans(line: str, x: float, y: float) -> str:
"""One line of IR as coloured SVG, using the same palette as the diff view."""
pieces = []
position = 0
for match in TOKEN.finditer(line):
pieces.append((line[position : match.start()], ""))
kind = match.lastgroup or ""
value = match.group()
if kind == "word":
kind = "keyword" if value in KEYWORDS else ""
pieces.append((value, COLOUR.get(kind, "")))
position = match.end()
pieces.append((line[position:], ""))

spans = []
for text, colour in pieces:
if not text:
continue
for text, colour in pieces(line):
fill = f' fill="{colour}"' if colour else ""
weight = ' font-weight="600"' if colour == COLOUR["keyword"] else ""
spans.append(f'<tspan{fill}{weight}>{html.escape(text)}</tspan>')
Expand Down
33 changes: 23 additions & 10 deletions toolkit/irx/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,36 @@
)


def highlight(text: str) -> str:
"""LLVM IR to coloured HTML. Small on purpose, see the module docstring."""
out: list[str] = []
def pieces(text: str) -> list[tuple[str, str]]:
"""Split IR into (text, colour) pairs, colour empty for anything uncoloured.

Three things render IR now: this module into HTML, the graph viewer into
SVG text, and the animation library into SVG text that moves. They agree
about what a keyword is because they all come through here.
"""
out: list[tuple[str, str]] = []
position = 0
for match in TOKEN.finditer(text):
out.append(html.escape(text[position : match.start()]))
out.append((text[position : match.start()], ""))
kind = match.lastgroup or ""
value = match.group()
if kind == "word":
kind = "keyword" if value in KEYWORDS else ""
if kind and kind in COLOUR:
weight = ";font-weight:600" if kind == "keyword" else ""
out.append(f'<span style="color:{COLOUR[kind]}{weight}">{html.escape(value)}</span>')
else:
out.append(html.escape(value))
out.append((value, COLOUR.get(kind, "")))
position = match.end()
out.append(html.escape(text[position:]))
out.append((text[position:], ""))
return [(chunk, colour) for chunk, colour in out if chunk]


def highlight(text: str) -> str:
"""LLVM IR to coloured HTML. Small on purpose, see the module docstring."""
out: list[str] = []
for chunk, colour in pieces(text):
if colour:
weight = ";font-weight:600" if colour == COLOUR["keyword"] else ""
out.append(f'<span style="color:{colour}{weight}">{html.escape(chunk)}</span>')
else:
out.append(html.escape(chunk))
return "".join(out)


Expand Down
86 changes: 86 additions & 0 deletions toolkit/irxmanim/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Short animations for the lessons, with no dependencies and no script.

The name is borrowed from manim, and so is the vocabulary: mobjects, a scene,
and `play` for one beat of it. None of the implementation is borrowed. manim
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. What comes out of here is a single inline `<svg>` with a stylesheet in it,
which animates in a notebook, in Colab's output sandbox, on the site, and as an
ordinary `<img>`.

from irxmanim import Scene, Box, link, fade_in, draw, highlight

s = Scene(420, 150, caption="A pass reads a function and writes one back")
ir = s.add(Box(20, 40, text="the function", tone="plain"))
pass_ = s.add(Box(230, 40, text="InstCombinePass", tone="flow"))
arrow = s.add(link(ir, pass_))
s.play(fade_in(ir))
s.play(draw(arrow))
s.play(fade_in(pass_))
s.play(highlight(pass_))

Print the scene and you get the storyboard, which is what a review reads. Show
it in a notebook and you get the picture. Call `svg()` and you get a file.

Everything about how it is drawn is in `mobject`, and everything about how it
moves is in `scene`.
"""

from __future__ import annotations

from . import mobject, scene
from .mobject import (
TONES,
Arrow,
Box,
Caption,
Code,
Group,
Head,
Line,
Mobject,
MobjectError,
Wash,
link,
)
from .scene import (
MAX_SECONDS,
Anim,
Change,
Scene,
SceneError,
draw,
fade_in,
fade_out,
highlight,
move,
pulse,
)

__all__ = [
"MAX_SECONDS",
"TONES",
"Anim",
"Arrow",
"Box",
"Caption",
"Change",
"Code",
"Group",
"Head",
"Line",
"Mobject",
"MobjectError",
"Scene",
"SceneError",
"Wash",
"draw",
"fade_in",
"fade_out",
"highlight",
"link",
"mobject",
"move",
"pulse",
"scene",
]
Loading
Loading