Skip to content

Three more animations: the stack machine, a dict lookup and a cycle - #67

Merged
tamnd merged 1 commit into
mainfrom
animations-three
Aug 29, 2026
Merged

Three more animations: the stack machine, a dict lookup and a cycle#67
tamnd merged 1 commit into
mainfrom
animations-three

Conversation

@tamnd

@tamnd tamnd commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Five animations now exist, which is the M1 item, and the set between them draws all nine primitives and all six named objects.

a03, the stack machine

One call to def area(w): return w * 2 + 1, run one instruction at a time, with the value stack on screen beside the listing. A disassembly listing tells you what each instruction is called and nothing about what it does to the pile of values underneath, and the pile is the part that is actually hard for a beginner. The function was chosen for its stack profile rather than for what it computes: the depth goes 0, 1, 2, 1, 2, 1, and those two climbs and two drops are the whole idea.

The instructions and the depths are from pyxray.stack.walk on 3.15.0rc1. The listing says LOAD_FAST_BORROW rather than LOAD_FAST because that is what the compiler emits here, and a reader who disassembles the function themselves should see what we showed them.

a04, how a dict finds a key

{1: "one", 5: "five", 9: "nine"} and then d[9], which collides. Since 3.6 a dict has not been one table of key and value pairs: there is a small array of slot numbers and a separate list of entries in the order they went in, and the two questions people actually ask about dicts both follow from that split. Key 9 lands on slot 1, finds key 1 there, probes again, and finds it in slot 6 on the second look.

Integer keys, because their hash is the value and is not randomized, so the picture is the same on every run. The index array on screen is [-1, 0, -1, -1, -1, 1, 2, -1] and it was not derived from the probing rule, it was read out of a live interpreter through ctypes at the address of the dict. test_xraymanim_facts.py does the same read and fails if CPython ever lays a small dict out differently.

a05, a cycle, and what frees it

Two objects that hold each other. With both names bound each count is 2, one from the name and one from the other object. After del a, b each is 1, and one is not zero, so nothing frees them and nothing can reach them either. Then the collector, which is smaller than people expect: it copies each count, subtracts one for every reference it finds from one tracked object to another, and a copy that reaches zero was only ever being kept alive from inside the group.

The counts are read out of the header with ctypes rather than through sys.getrefcount, because passing an object to a function is itself a reference and this animation is about counting references exactly. gc.collect() returning 2 is checked too.

The tally in tear_down had never run

This is the thing worth reading twice. Explainer.tear_down checks that a scene played the beats its storyboard planned, and that check is the reason the storyboards are worth having at all. The guard that switches it off for a partial render read config.from_animation_number > 0 or config.upto_animation_number >= 0. Manim's default for the upper bound is math.inf, not -1, and math.inf >= 0 is true, so every render took the early return. It had never once run, including in CI.

It turned up because a03 played seven of its eight beats and rendered without a word. Proved by rendering a deliberately broken scene, which exited 0 before the fix and raises AssertionError: a03-the-stack-machine played 1 beat(s) but its storyboard has 8 after it. There is now a test that asserts the default is infinity and says in its docstring why that line exists.

Fixes to the shared library

Frame drew its value stack hanging from the rule at the top, so it grew downward as values were pushed. Every stack in this project grows upward without exception, and a reader who sees that broken once reads every later picture wrong. The stack now sits on the floor of the frame, an empty stack is the floor with nothing on it rather than a gap, and the locals and the stack are both captioned.

arrow takes a bend, which is for the one case a straight line cannot draw: two objects pointing at each other come out as one line drawn twice. Bent by the same angle the pair comes out as the lens shape everybody draws a cycle as, because the two arrows have their ends swapped.

graph works out which side of a box an edge leaves from, instead of always leaving from the bottom, which is right for a tree and wrong for everything else. Positions are still given by hand.

DictTable writes the slot numbers outside the index array. A slot number is a position, in the same way a list index is, and CPython does not store it anywhere, so it is written beside the array and not in it. Without them a reader has to count cells to find slot 6, and a picture that has to be counted is a picture that gets read wrong. The gap between the two arrays is wider too, because they are two separate pieces of memory and every arrow in a04 is drawn across it.

VISUAL-SYSTEM.md is amended for all three, which is the rule the library enforces on itself.

Checks

just check is green: 970 tests, 352 citations, 12 lessons, 12 notebooks, 2 blueprints, 96 diagrams, 5 animations. The manim half of the suite is 71 tests and passes under --extra anim. The three GIFs are 1.7 MB, 1.2 MB and 1.1 MB, all well under the 6 MB limit.

Part of #20.

a03 runs one call to `def area(w): return w * 2 + 1` one instruction at a time
with the value stack drawn beside the listing. A disassembly listing tells you
what each instruction is called and nothing about what it does to the pile of
values underneath, and the pile is the part that is hard. The instructions and
the depths come from `pyxray.stack.walk` on 3.15.0rc1, which is why the listing
says LOAD_FAST_BORROW and not LOAD_FAST.

a04 is a dict lookup that collides. Since 3.6 a dict has been a small array of
slot numbers and a separate list of entries in insertion order, and both of the
questions people ask about dicts follow from that split. The eight slot array on
screen was read out of a live interpreter through ctypes rather than derived
from the probing rule, and a test does the same read and fails if CPython ever
lays a small dict out differently.

a05 is two objects that hold each other. Take the names away and both counts
drop to one, which is not zero, so nothing frees them and nothing can reach
them either. Then the collector's trick, which is smaller than people expect:
copy each count, subtract one per reference found inside the group, and a copy
that reaches zero was only ever being kept alive from inside.

The tally in tear_down had never run once. The guard read
`config.upto_animation_number >= 0`, but manim's default for that is math.inf
rather than -1, so every render took the early return, including the real ones.
It was found because a03 played seven of its eight beats and rendered without
complaint. There is now a test that pins the default down and says why.

Frame drew its value stack hanging from the rule, so it grew downward as values
were pushed, which contradicts the one rule the visual system states without
exception. It now sits on the floor of the frame and grows up, an empty stack
is the floor with nothing on it, and both regions are captioned.

arrow can bend, which is the one case a straight line cannot draw: two objects
pointing at each other come out as one line drawn twice. graph works out which
side of a box an edge leaves from instead of always leaving from the bottom,
and passes the bend on. DictTable writes the slot numbers outside the array,
because a slot number is a position and not a thing CPython stores, and a
picture whose cells have to be counted gets read wrong.

The five animations between them now draw all nine primitives and all six named
objects, and there is a test that says so.
@tamnd tamnd added kind/animation A manim scene or the mobject library area/pedagogy Sequencing, the beginner ramp and assessment labels Aug 29, 2026
@tamnd
tamnd merged commit 33b3aa4 into main Aug 29, 2026
8 checks passed
@tamnd
tamnd deleted the animations-three branch August 29, 2026 00:51
@tamnd tamnd mentioned this pull request Aug 29, 2026
19 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/pedagogy Sequencing, the beginner ramp and assessment kind/animation A manim scene or the mobject library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant