12. Visualization
A visualization shows what a program does inside the machine: the frames on the call stack, the objects on the heap and the references between them, the moment a value is moved or borrowed, the order in which threads and tasks run, the bytes of a struct. These are the things that a programmer must imagine and that prose explains badly. This document extends the visualization system of algorithm (2150/10: the trace protocol, the primitives, the player, the checks) with the primitives that programming needs, with traces recorded from real runs, with diagrams generated from code and data, and with screenshots. Where this document is silent, 2150/10 applies.
12.1 Principles
- The picture is the real run. A scene of an example shows the run that CI executed (07.4): the same code, the same image, the same output. A picture that the build cannot tie to a run is a figure (V1), not a scene (V2).
- Record first, author second. Where a tracer exists for the language (12.4), the scene is a recorded trace of the example, drawn by the player. An author writes a scene by hand only for models that no tracer can see (the event loop's queues inside a runtime, a borrow, a cache line), and the check of 12.11 ties it to a real run.
- The algorithm principles stay. The reader drives, one step is one line, trace then draw, the same primitives everywhere, readable without colour, motion or script (2150/10.1).
- Show the model the page teaches. The heap view of a Python page shows names bound to objects. The heap view of a C page shows bytes at addresses. The primitives have views for each model (12.3), and a scene uses the view of its language's model.
- Generated, never drawn by hand. Class diagrams, sequence diagrams, railroad diagrams, query plans, git graphs and the genealogy come from code, grammars, tool output or the graph. A diagram file drawn by hand in
figures/is allowed only for ideas that have no data source, and it must have a text alternative (V09).
What we learn from others, without copying their code or assets (19): Python Tutor (Philip Guo) shows that a view of frames and objects teaches references and aliasing better than prose; the Rust community's ownership diagrams show that a timeline of moves and borrows makes the borrow checker readable; explain.depesz.com and similar tools show that a query plan is easier to read as a tree with costs than as text.
12.2 Scene sources
| Source | What it is | Used for | Tied to the run by |
|---|---|---|---|
| recorded | a trace that a tracer writes while the example runs in its image (12.4) | stack, heap, references, closures, recursion, exceptions, generators, threads in languages with a tracer | the same CI run: the trace and the output block come from one execution (V03) |
| authored | viz/scene.ts, as in algorithm, using @prog/viz |
the event loop, ownership and borrowing, memory models, GC phases, isolation anomalies in SQL, anything a tracer cannot see | the line-sync check against the real run (V06) and the declared failures (V07) |
| generated | a diagram that the build makes from a source: code, a grammar, tool output, the graph | class and sequence diagrams, ASTs, railroad diagrams, query plans, the git DAG, memory layout, the genealogy (12.5, 12.6) | the generator runs on the node's own code or data in the image |
| captured | a screenshot of a GUI or a rendered page (12.7) | GUI examples, HTML and CSS rendering | the scheduled job that runs the example |
@prog/viz is @prog/viz (the same package source, copied as in 03) plus the primitives of 12.3. It lives in tamnd/programming-agent under MIT. The trace protocol (steps, events, line, note, vars, phase, counters) and the state vocabulary (active, compare, visited, path, rejected, invariant, ...) are the same as in algorithm, so the player is one player.
12.3 New primitives
| Primitive | Draws | Views | Events (beyond the algorithm set) | Used by (examples) |
|---|---|---|---|---|
frames |
the call stack: one box per frame with the function name, the line, the local names and their values | column (newest on top), tree (for recursion trees) |
call(fn, args), ret(value), unwind(exc), bind(frame, name, ref), rebind, del |
functions, recursion, scope, closures, exceptions |
heap (objects) |
objects as boxes with a type and fields; references as arrows from names and fields | names (Python, JS, Java, C#, Go: names point to objects), boxes (values inline, pointers as arrows: C, C++, Rust, Go structs), bytes (a memory range with addresses) |
alloc(id, type, fields), field(id, k, ref), free(id), collect(ids), refcount(id, n), addr(id, address) |
references, aliasing, copies, mutability, GC, pointers, dangling pointers |
ownership |
one lane per value or binding, with spans for "owned", "borrowed shared", "borrowed mutable", "moved out" | timeline |
own(var, value), move(from, to), borrow(var, kind, span), drop(var), conflict(line, code) |
ownership, moves, borrowing, lifetimes, RAII, C++ move semantics |
lanes |
one lane per thread, task or process; steps as blocks on the lanes; shared variables as rows; happens-before arrows | lanes, interleaving (one linear order of the steps, chosen by the reader) |
spawn(t), join(t), run(t, line), read(t, var), write(t, var, v), lock(t, m), unlock, wait, notify, send(ch, v), recv, race(a, b), deadlock(cycle) |
threads, data races, locks, deadlock, channels, actors, memory models |
eventloop |
the call stack, the task queue, the microtask queue, the timers and the I/O sources of an event-driven runtime | js (the HTML event loop model), node (the libuv phases), asyncio, generic |
enqueue(q, task), dequeue(q), tick, await(promise), resolve(promise, v), phase(name) |
async/await, promises, callbacks, setTimeout ordering, Python asyncio |
layout |
a type's bytes: fields as ranges at offsets, padding as hatched ranges, alignment marks, the total size | row (bytes in a line), grid (by word) |
field(name, offset, size), pad(offset, size), align(n) |
struct padding, alignment, repr(C), unions, endianness, enum tags |
dag (git) |
commits as nodes, parents as edges, branches and tags as labels, HEAD, the index and the working tree as side panels |
lanes (one column per branch), graph |
commit(id, parents, msg), ref(name, id), head(ref), stage(path), checkout, merge, rebase(from, onto), conflict(path) |
Git branching, merge, rebase, reset, reflog, cherry-pick |
ast |
a parse tree or an abstract syntax tree, linked to the source text ranges | tidy, indented |
node(id, kind, range), edge(parent, child, field), focus(range) |
parsing, precedence, macros, the language-implementation area |
derivation |
an inference-rule tree: judgments with the rule name on each bar | rules |
judge(id, ctx, term, type), rule(name, premises), unify(a, b, subst), fail(reason) |
type checking, type inference, the formal sections (11) |
automaton |
an NFA or a DFA with states, transitions and the input with a cursor; for backtracking engines, the stack of choice points | graph, backtrack |
state(id, accept), trans(a, b, label), active(ids), consume(i), choice(push or pop), match(range) |
regex matching, backtracking, catastrophic backtracking and ReDoS, lexers |
plan |
a query plan tree with the operator, the estimated and the actual rows and costs | tree, flame |
op(id, kind, est, actual), edge(parent, child) |
SQL query plans, indexes, joins |
sequence |
a sequence diagram: participants as columns, messages as arrows in time order | uml |
participant(id, name), msg(from, to, label, kind), activate, deactivate |
HTTP request and response, RPC, method calls between objects, TLS handshake as programmers see it |
classes |
a class diagram: types with members, and the relations (inherits, implements, composes, uses) | uml |
none (static) | patterns (06.3), object-oriented features |
railroad |
a syntax diagram for a grammar production | railroad |
none (static) | feature synopses (06.3), grammars (11.5), regex syntax |
bits (extended) |
a machine word with the sign, exponent and mantissa fields for IEEE 754, two's complement for integers, UTF-8 bytes of a code point | int, float, utf8 |
set(i), field(name, range), overflow |
integer overflow, floating point, text encoding |
Rules for primitives are as in algorithm: a scene uses only the primitives of @prog/viz, and a new primitive is a change to the package with its own review (V13).
12.4 Recorded traces
A tracer runs an example inside its image and writes a trace file. The tracer runs in the same CI job as the example's run (07.4), so the trace, the output and the run status come from one execution.
Tracers by language (Default; the list grows with tier 2):
| Language | Tracer | How | From |
|---|---|---|---|
| Python | prog-trace-py |
sys.monitoring (PEP 669) events for line, call, return, raise and yield; sys.settrace for versions before 3.12 |
M2 |
| JavaScript, TypeScript | prog-trace-js |
source instrumentation: the compiled JS is rewritten to call a recorder at each statement and call (Default); the V8 inspector protocol with stepping as the fallback for code that the rewrite cannot handle | M2 |
| Java, Kotlin | prog-trace-jvm |
the Java Debug Interface (JDI): step events, frames and object fields through a debugger that attaches to the run | M3 |
| C, C++ | prog-trace-native |
gdb with its Python API: step by line, read frames, locals and the memory of pointed-to objects; rr to record once and replay, so stepping does not change timing |
M3 |
| Rust, Go, others | the native tracer with gdb or lldb (Rust), Delve (Go) | as for C | M4, Default |
The trace file (build/traces/<node>/<example-id>.json, gzip) holds the algorithm trace format (2150/10.2.3) with these additions:
| Field | Meaning |
|---|---|
run |
the run key of 07.4 (image digest, wrapper, flags, block hash) and the run status |
source |
the file and the line of each step, in the code that the reader sees (the wrapper's lines are removed and the line numbers mapped, 07.3) |
frames |
for each step: the frames with the function, the line and the local bindings; a binding is a primitive value or a reference to a heap id |
heap |
for each step that changes it: the objects with id, type and fields, as a diff from the step before |
threads |
the thread or task of each step, for lanes |
out |
the stdout and stderr chunks written at each step |
truncated |
true when the run passed the step budget (12.10); the trace keeps the first steps and the last steps |
Filters. A tracer records only the example's own code. Steps inside the standard library are folded into one step ("calls sorted, returns ..."). Objects of the runtime that the reader did not make (modules, the builtins) are hidden. The per-language filters are in languages/<lang>/trace.yaml.
Values. Values are shown as the language prints them (repr in Python, the debugger's format in C), with the per-language normalizers of 07.6 for addresses and identity hashes. In the bytes view, real addresses are kept, because the address is the point; the trace stores them, and the build marks them as "addresses from one run".
A trace is published only for a block at X2 or better. When the block's output changes (a new image), the trace is recorded again in the same run.
12.5 Generated diagrams
| Diagram | Source | Generator | Where it appears |
|---|---|---|---|
| class diagram | the node's example code | symbol extraction with tree-sitter (the treesitter field of 08.4): types, members, inheritance and implementation; composition from field types |
pattern pages (06.3, Solution), object-oriented features |
| sequence diagram | a recorded trace (12.4): calls between objects of the example's types | the trace filtered to calls across types | pattern pages, RPC and HTTP pages |
| AST and parse tree | the example's code | the language's own parser where it has one in the image (Python ast, the TypeScript compiler API, go/ast, syn for Rust), tree-sitter otherwise |
parsing, precedence, metaprogramming, language implementation |
| railroad diagram | the grammar productions of the feature (11.5) | our renderer, from EBNF or PEG | feature synopses (06.3), grammar pages, regex syntax |
| type derivation | the formal development (11) or the inference engine of Prog.Inference |
the derivation printed by the Lean code | type checking and inference pages |
| query plan | the example's SQL run in the image | EXPLAIN (ANALYZE, FORMAT JSON) in PostgreSQL, EXPLAIN QUERY PLAN in SQLite, and the plan output of the other engines that the page shows |
SQL pages, indexes, joins |
| git DAG | a shell example of Git commands run in the image | the build reads the repository after each command (git log --all with a fixed format, git status) and emits dag events, one step per command |
Git pages (06.3, tool) |
| memory layout | a probe program in the image that prints the size, the alignment and the offset of each field | per target: x86_64 and aarch64 images |
struct layout, padding, repr(C), unions |
| automaton | the regex of the example | our Thompson construction and subset construction for the NFA and DFA; a backtracking simulator for the backtrack view |
regex pages, ReDoS |
| request and response sequence | the log of the local fixture server (07.5, fixture=http) |
the build reads the requests and responses that the fixture saw | HTTP, REST, client library pages |
Rules:
- A generated diagram is rebuilt when its source changes. It is never edited by hand.
- A diagram from our own simulator (the automaton, the backtracking view) must agree with the real engine: the match result of the simulator equals the match result of the engine in the image, on the example and on the cases of the node (V08).
- A diagram from tool output records the tool's version and shows it ("plan from PostgreSQL 18").
12.6 The genealogy and the release timeline
The genealogy is the graph of influenced_by and variant_of edges between languages (04.6), drawn by year.
- Data. Each edge has a source (04.6). The year of a language is
appeared. Releases come fromreleases.yaml(09.1) for languages of tiers 1 to 3 and from release cards for the others. - Views. The family view (one family of 08.7, 10 to 200 languages) is a layered graph with time on the x axis. The language view shows one language with its ancestors and descendants to depth 2, and its release timeline as a row of marks with the support window (09.3) shaded. The full view (all languages with an edge, about 5,000, Estimate) is a canvas with zoom and a filter by family and by tier, loaded only on
/languages/genealogy/. - Static first. Each family view and each language view is an SVG made at build time, with the text alternative of 12.9. The interactive view adds filters, hover and links on top of the same layout.
- Release timeline. The language page (06.3, "Versions and support") shows the releases with their dates, end-of-life dates and the computed status (09.5). The release timeline of several languages on one axis is part of the comparison pages (08.8).
- Layout is deterministic: the same graph gives the same picture, so a diff of the SVG shows real changes only.
12.7 Screenshots
GUI examples (07.5, norun reason=gui) and HTML and CSS pages that show rendering get captured images instead of scenes.
- A scheduled job runs the example with a virtual display (Xvfb for Linux GUI toolkits) or in headless Chromium and Firefox (for HTML and CSS), at fixed sizes (a desktop and a phone width), and saves PNG and AVIF.
- The image records the toolkit or browser version, the OS image and the date, and shows "captured on ".
- A capture whose pixels change between two runs with the same inputs is flagged as unstable, and the page uses the older capture until a person looks at it.
- A screenshot is a V1 figure. It never counts as V2.
- Screenshots of other people's products are not used, except under nominative use for a tool's own interface, captured from our own run (19).
12.8 The player and code sync
The player is the algorithm player (2150/10.3) with two changes:
- Code sync uses the real code. The code panel shows the example as the reader copies it (07.3), and the highlighted line is the line of the trace's
source. There is no pseudocode panel on programming pages. When a concept page shows the same scene in several languages (a closure in Python, JavaScript and Rust), each language has its own trace, and the language tabs switch the trace. - Side panels. A scene may show up to three primitives at once, in a fixed layout: the main view (for example
heap), theframescolumn on the left, and the output console at the bottom (from the trace'sout). The console shows the output up to the current step, so the reader sees which line printed what.
Input editing works only for scenes with an input schema and a browser runtime (07.10): the reader changes the input, the browser runs the tracer of that runtime (Default: Pyodide for Python at M3; others later), and the player draws the new trace. For other languages the presets are the inputs.
12.9 Accessibility
As in algorithm (2150/10.8.1), and:
- Text alternative for every scene and figure. The text view (
t) of each new primitive is defined:framesas a nested list of frames and bindings;heapas a table of objects with their fields, and a list of "name refers to object" lines;ownershipas a table of variables with the line ranges of each state;lanesas a table of steps with the thread, the line and the shared reads and writes;eventloopas the contents of each queue;layoutas a table of fields with offset, size and padding;dagas the output ofgit log --graph --oneline;astandderivationas indented outlines;automatonas a transition table;planas the indented plan text;sequenceas a numbered list of messages;classesas a list of types with members and relations;railroadas the EBNF text. Each generated SVG has the same text in a<desc>and in a fold under the figure. - Keyboard step control.
→and←step,HomeandEndgo to the start and the end,Spaceplays and pauses,[and]jump to the previous and next phase,ttoggles the text view,1,2,3move focus between the panels. Every control is also a button with a label. - Narration. Each step of a recorded trace gets a generated note ("
counteris bound to a new int object 1"), from templates per event and language, so that a screen reader can follow a recorded scene that no author wrote notes for. Authored scenes have notes written by the author, as in algorithm. - Colour. The state marks of 2150/10.2.5 apply. Ownership states have patterns (solid for owned, dotted for shared borrow, double for mutable borrow, crossed for moved out).
12.10 Size budget
| Budget | Value |
|---|---|
@prog/viz runtime, gzip |
at most 90 KB (the 60 KB of @prog/viz plus at most 30 KB for the new primitives), loaded once, only when a player scrolls into view |
| one authored scene bundle, gzip | at most 15 KB |
| one trace file, gzip | at most 200 KB; the default preset's trace at most 50 KB |
| steps in a recorded trace | at most 2,000; the tracer stops at the limit and sets truncated |
| heap objects drawn at once | at most 150; more objects are grouped ("+ 40 more int objects") |
| one generated SVG | at most 150 KB before gzip; the genealogy full view is a canvas with tiled data, at most 2 MB gzip, loaded only on its own page |
| screenshots | AVIF at most 120 KB each, PNG fallback at most 400 KB |
| playback and first draw | as in algorithm: 60 frames per second, first draw within 100 ms of load, no layout shift |
All numbers are Default and are checked by V10.
12.11 Checks
prog viz check <node> runs these checks in CI for each changed scene, trace or diagram, and the nightly build runs them all. They are rules of the audit (15).
| Rule | Check |
|---|---|
| V01 | an authored scene builds, imports only @prog/viz, and uses no DOM, network, timer or unseeded random |
| V02 | determinism: the same input gives the same trace hash twice; a recorded trace gives the same trace hash on a re-run in the same image, after the address and identity normalizers |
| V03 | a recorded trace comes from the same run as the example's output block: the run key matches the run key of the block in build/runs/, and the trace's out equals the recorded output after normalization (07.6) |
| V04 | every source line of a trace is a line of the example as the reader sees it (no wrapper lines) |
| V05 | the presets of an authored scene reach every line of the example that the scene claims to show |
| V06 | line sync for authored scenes: the sequence of lines of the authored scene equals the sequence of lines of a recorded or instrumented run of the same example, with repeats merged; for languages with no tracer, the build instruments the example with print markers at each line, runs it, and compares |
| V07 | declared failures agree: an ownership conflict(line, code) event, a lanes race or deadlock event, or an unwind event matches a fails block of the node with that signature (07.2): the same diagnostic code at the same line, the same sanitizer kind (ThreadSanitizer for a race), the same exception type |
| V08 | a diagram from our own simulator agrees with the real engine or tool on the example and the node's cases (12.5) |
| V09 | every scene, trace and figure has a text view or a text alternative; every step has a note (written or generated); every state used is in the vocabulary |
| V10 | the budgets of 12.10 hold |
| V11 | the filmstrip and the social image render, as in algorithm |
| V12 | each control of an explorable has a label and a range, and each value of the range draws without error |
| V13 | the scene uses only the primitives of @prog/viz |
| V14 | a generated diagram's source hash matches the current source (no stale diagram), and the tool version it records is the version in the image |
| V15 | a screenshot is younger than the newest image of its toolkit or browser by at most 90 days, or the page shows it as "captured with " |
V03 and V07 are the checks that matter most. They make a scene a picture of the real run, and they make an error marker in a scene the same error that the compiler or the sanitizer gave.
12.12 The V ladder and what needs a scene
The levels are the ones of 04.9:
| Level | It needs |
|---|---|
| V0 | no scene, no trace, no figure |
| V1 | static figures: generated diagrams, screenshots, or a filmstrip from a scene that fails a check needed for V2 |
| V2 | an interactive scene (recorded or authored) or an explorable that passes V01 to V14 as they apply, with step back, code sync on the real code, the text view and a note on each step |
| V3 | V2, and a person who is not the author reviewed it for clarity and accessibility on this exact hash (15.2) |
By kind, for a node of the core:
| Kind | Needs |
|---|---|
concept with viz: expected |
V2 (and V3 for "complete", 04.9) |
pattern |
V1: the generated class diagram and sequence diagram (06.3) |
feature |
optional; V2 for the features that realizes a viz: expected concept in a tier-1 language, through the concept's scene in that language |
task |
optional |
tool |
V2 for the core concepts of Git (the object model, branches, merge, rebase, reset); V1 for other core tools (a screenshot of first use when it has a UI) |
language |
V1: the genealogy view and the release timeline, generated |
weakness |
V1; V2 for the memory-safety weaknesses (a heap scene in the bytes view of the overflow or the use after free, from the fails example under ASan) |
format |
V1 for encodings (the bits view of UTF-8), otherwise none |
| others | none |
Concepts with viz: expected (the first list, about 60, Estimate; the curator sets the field and the editors approve it at M1):
| Area | Concepts |
|---|---|
| foundations | two's complement and integer overflow, floating point representation, text encoding and UTF-8, stack and heap |
| functions-and-abstraction | the call stack, recursion, tail calls, scope and binding, closures, generators and coroutines, lazy evaluation, continuations |
| data-and-values | value and reference semantics, aliasing, mutability, shallow and deep copy, equality and identity |
| objects-and-classes | dynamic dispatch and vtables, inheritance and method resolution order, prototypes |
| memory-and-resources | pointers, dangling pointers, manual allocation, reference counting and cycles, tracing GC (mark and sweep, copying, generational), ownership and moves, borrowing, RAII, memory layout and alignment |
| errors-and-control | exceptions and stack unwinding, finally and defer order |
| concurrency-and-parallelism | threads and interleaving, data races, locks and mutual exclusion, deadlock, channels, actors, the event loop, async/await, promises and microtasks, memory models and atomics |
| syntax-and-semantics, types | operator precedence and the AST, evaluation order, type checking, type inference |
| data-and-persistence | regex matching, backtracking and ReDoS, SQL joins, indexes and query plans, transaction isolation anomalies |
| tools-and-workflow | Git commits and branches, merge, rebase, the index |
| web-development | the HTTP request lifecycle, DOM event propagation, the CSS box model, flexbox, grid |
12.13 Authoring
prog new viz <node> [--from-trace <example-id>]. With--from-trace, the command records the trace of the example and writesviz/scene.yaml: the example id, the primitives and views, the presets, and the note templates to override. No code is needed for a recorded scene.- For an authored scene, the command writes
viz/scene.tswith onev.linecall per line of the example, as in algorithm. prog viz serve <node>opens the player with reload on save and the trace inspector.prog viz check <node>runs the checks of 12.11 and prints the V level before and after.- The
visualizeragent (14) follows the same steps. It prefers a recorded scene and writes an authored scene only when the concept is in the list of models that no tracer can see (the event loop, ownership, memory models, GC phases, isolation anomalies).