summaryrefslogtreecommitdiff
path: root/graph/README.md
blob: f0e32d9d477feed0ca412b81b907904d3a38ce5d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# Shared graph viewer

The browser application lives in `web/` and is shared by chapters 1–25.
`web/index.html` contains the page and `web/viewer.js` manages playback.
`web/groups.js` projects folded regions into display nodes; `web/layout.js`
runs ELK layout in a browser worker; `web/peep.js` builds temporary neighborhoods;
`web/render.js` draws SVG
and handles selection, pan and zoom. Libraries live in `web/vendor/`, with one checked-in
copy of each; chapters do not copy them during builds.

From a chapter directory, run `make view`. The shared `GraphViewer` finds
`graph/web/index.html` by walking up from the working directory, serves its
directory through the JDK's HTTP server on a local ephemeral port, and opens
that web address in the browser. This works from a chapter directory, the
repository root, or the root of a linearized checkout. Java launches from elsewhere can specify
`-Dsimple.graph.url=<viewer URL>` before the main class.

The viewer uses a WebSocket connection. Every chapter sends structured
JSON snapshots in each frame.
No JavaScript package installation, frontend build, or separate server process
is required.
For development over HTTP, run this from the repository root:

```sh
python -m http.server 8000 --bind 127.0.0.1 --directory graph/web
```

Then launch Java with `-Dsimple.graph.url=http://127.0.0.1:8000/index.html`.
The compiler WebSocket still uses port 12345; run one viewer session at a time.

The initial program compiles on connection. For another program, click **Compile**
or press **Ctrl+Enter** (**Cmd+Enter** on macOS). Use the arrows to step through
frames (Left/Right arrow keys perform the same steps, including animation stages),
the first/last buttons to jump to either end, or enter a frame number
and press **Enter**. The slider scrubs through available frames; its arrow keys
step and **Home/End** jump to either end. Inputs keep their own arrow-key behavior;
the playback shortcuts apply outside inputs, without modifier keys.
Keep `make view` running while using the tab.
Frame numbers start at 1. Jumps lay out only their destination. When the slider
moves during layout, the viewer finishes that calculation and then handles the
latest request, skipping intermediate requests. Already visited layouts stay cached.

Chapters 10a, 10b, and 11–25 run their existing TypeCheck pass after optimization. A successful
check adds a TypeCheck phase-complete frame. An error adds a final graph frame
with the compiler's diagnostic; stepping or jumping to it centers the error node
at a readable scale, outlines it in red, and shows the message on the phase line
and in node details. Folded regions containing that node are revealed for this
frame without changing earlier frames' fold choices. The graph and history stay
available. Errors without an associated node still show their message.

Structural peepholes take three forward clicks: **gather**, **rewrite**, then
**release**. Gathering slides the edit and its context into a highlighted enclosure.
The rewrite adds/removes nodes and reconnects edges there; surviving nodes keep
their positions. Release restores the ordinary post-peep graph and the previous
view as closely as possible. Backward clicks reverse these stages. Type-only
updates and unchanged results remain one-click steps without gathering or zooming.

Call, CallEnd, Fun and Return nodes being folded away by inlining have a small
**FOLDING** tag and a pink fill. This reflects the compiler's folding flag,
including between peeps, and is separate from the viewer's region fold buttons.

The enclosure includes nodes changed between the snapshots, the peep root and
replacement, and their direct defs in either snapshot, excluding constants' Start
anchors. An existing GVN replacement is gathered alongside the original even when
the replacement itself is unchanged. Defs do not recursively bring their own
inputs; recorded scheduling dependencies do
not expand the enclosure. It reserves space for both versions, keeps Multi/projection boxes
intact, and places gathered Phis beside their Region without pulling in sibling
Phis. Unrelated graph content is subdued. Affected folded
regions are temporarily revealed; release restores the user's folds. Nodes one
edge outside it remain more visible than the distant graph. Playback pairs an
enclosing peep's BEFORE with its matching RETURN/APPLY, treating recursive peeps
as part of that edit. A replacement built and typed by a nested peep therefore
appears on the rewrite click, never before gathering its parent's neighborhood.
When a RETURN precedes the caller attaching its replacement, playback can use
the next capture as the result: every intervening node change must be a use
switching from the old root to the replacement, or deletion of the root and its
unused inputs. This shows the actual rewiring/removal in the enclosure. Unrelated
parser edits are not combined. The following BEFORE can serve both as this
edit's result and as the next edit's starting point.

Gathered/rewritten views are intermediate states, identified in the event line.
Frame numbers, the slider, and first/last buttons always jump directly to stable
snapshots. Capture numbers are retained, so stepping can skip numbers belonging
to nested attempts or a RETURN waiting for its caller's attachment. Entering or
scrubbing to one of those numbers lands on the enclosing BEFORE frame.
A jump or Escape cancels an intermediate view. Reduced-motion mode
keeps the same inspectable stages but changes views immediately.

Functions, loops and closed If/Region diamonds have enclosing boxes. Click **−** in a box's upper-left
corner to fold it into a thick-bordered node; click **+** to reopen it.
**Unfold all** restores the complete graph. Folds persist while stepping forward
or backward, including child folds inside a folded parent. Compiling a new
program resets them. Edges between the same visible use/def pair bundle when
either endpoint is folded. Bundles use thick lines: red if any member is control,
otherwise blue if any is memory, otherwise the data color. Pure associations
retain their dash pattern and visibility toggle. Hover for the original node IDs and input slots.
Selection and peephole highlights
follow hidden nodes onto their enclosing folded box.

Scroll to zoom, drag to pan, and use **Fit** to show the whole
graph again. Clicking a node shows its full label, type and input slots, and
highlights its edges. Selection follows that node's ID across frames; Escape
clears it. **Associations** shows scope bindings and lifetime edges, which are
hidden by default. Input slots retain their indices, including null slots.
Click the middle of an edge to pan to its use; click near the use to jump to
the def. Repeated clicks without moving the mouse alternate between the two.
The edge lands under the pointer just outside the node, preserving zoom, with
a short eased pan. Folded edges jump to the visible folded boxes. Moving the
mouse away, panning, zooming, Escape, Fit, or changing frames cancels the jump.
The pan is immediate when the browser requests reduced motion.
**Peep neighborhood** outlines nearby nodes in amber, with a darker outline
on the current node/replacement. The event label identifies the phase, rewrite,
and enclosing rewrite when calls nest. This checkbox controls the node outlines;
the enclosure remains visible throughout the intermediate animation stages.

For documentation figures, step to the desired frame and click **Save SVG**.
It saves the whole graph, regardless of pan/zoom, with its current association
visibility, neighborhood highlighting and selection. Clear selection with
Escape and uncheck **Peep neighborhood** for an unmarked diagram. The SVG
includes its styles and arrow markers and needs no JavaScript or viewer assets;
check it into the chapter's docs and embed it like the existing static figures.
The chapter IR figures also have detached `.json` snapshots. These preserve the
illustrated topology, including schematic pre-optimization states that the
parser does not retain. Node IDs in these illustrations are illustrative;
omitted type annotations are not inferred. Their SVGs use this viewer's actual
layout, node shapes, colors, projection boxes, Region/Phi rows, and group routing.
The old `.gv` files beside these snapshots retain the original figure topology;
when both exist, the JSON is the maintained source for the SVG.

To regenerate a snapshot, install Python Playwright and its Chromium browser,
then run from the repository root:

```sh
python graph/render_docs.py chapter01/docs/01-graph.json
```

Use `--browser msedge` or `--browser chrome` for an installed Chromium browser,
or `--browser firefox`. The command serves viewer assets on an ephemeral local
port and renders headlessly, without connecting to a compiler or a live viewer.
This optional documentation tool is not needed to build or test the chapters.
The chapter `docs/Makefile` rules select JSON before a same-named legacy DOT
file. They accept `PYTHON` and `DOC_BROWSER` overrides, for example
`make -C chapter01/docs PYTHON=python DOC_BROWSER=msedge` from the repository root.

For a before/after comparison, keep node IDs stable across the two snapshots
and place their SVGs in a two-column Markdown table:

```markdown
| Before | After |
|---|---|
| ![Before the rewrite](docs/before.svg) | ![After the rewrite](docs/after.svg) |
```

This keeps both graphs side by side in the chapter README. Chapter 10a's
[load-after-store example](../chapter10a/README.md#local-memory-optimizations)
uses this format to isolate a single rewrite.

The compact memory-dependency schematics in 10b retain `.gv` sources to
preserve their deliberate horizontal layouts. They use the viewer palette and
node shapes; definitions are left of uses horizontally and above uses vertically,
with arrows from use to definition. Type lattices use their own domain colors
and top-to-bottom lattice order, rather than IR edge conventions.

The launcher prints the viewer URL before opening the browser. If the desktop
only raises an existing browser window, paste that URL into a new tab. Use
`-Dsimple.graph.open=false` to suppress automatic browser opening when testing
or connecting a browser manually. A disconnected viewer stays open and shows
its connection status instead of silently closing the tab.

After building a chapter, `python graph/test_transport.py chapter25` from the
repository root checks HTTP asset delivery, a delayed and fragmented WebSocket
upgrade, and session shutdown without opening a browser.

`python graph/test_browser.py chapter25 --browser msedge` additionally checks
actual rendering, stepping backward/forward, compiling multiple programs, and
disconnected startup in a headless browser. This optional test requires the
Python `playwright` package and the selected browser; it is not a viewer runtime
dependency. Use `--browser firefox` after `python -m playwright install firefox`
for Firefox testing. Test logs/screenshots are written under `build/graph-browser/`.

## Boundary between chapters and the viewer

All chapters share the Java viewer plumbing as well as the browser:

- `GraphViewer` owns asset discovery, the source/compile loop and session shutdown.
  Each chapter launches it from `SimpleGraphObserver.main`; no separate launcher
  class is needed.
- `GraphSocket` serves local assets, opens the browser, and handles WebSocket
  messages. One buffered stream handles both the upgrade and frames. Payloads
  use exact reads and their advertised length, including 64-bit lengths;
  continuation frames assemble UTF-8 before decoding, and PING data is echoed.
- `GraphCapture<N>` owns nested attempts, buffered snapshots, event numbering,
  neighborhood accumulation, serialization and disconnect handling. It extends
  `GraphObserver<N>`. Chapter subclasses compile source, attach/detach the observer,
  and supply roots, phase and parser position. Each compilation gets fresh capture
  state and a new compilation key, even when the previous compilation failed.
- `BaseNode<N>` in `print/` supplies identity and indexed graph/dependency access.
  Dependency access defaults to an empty set. `GraphAdapter<N>` adds viewer roles
  and grouping facts for the chapter.

Shared Java has no dependency on chapter classes.
The WebSocket listener and built-in asset server bind to 127.0.0.1.

The connection messages are:

| Direction | Message | Meaning |
| --- | --- | --- |
| Compiler to browser | `!` | Request the source program |
| Browser to compiler | Source text | Compile and capture a new sequence |
| Compiler to browser | JSON `{snap, pos, evt}` | Complete graph, parser position and event |
| Compiler to browser | JSON `{error}` | Compilation failed; the session accepts another program |
| Browser to compiler | `+` | Acknowledge/request another frame |
| Compiler to browser | `#` | End of sequence |
| Browser to compiler | `null` | End the session |

The JSON frame has this shape:

```json
{
  "snap": {
    "ver": 1, "comp": "compilation UUID", "step": 0, "roots": [1], "scope": 0,
    "nodes": [
      {"id": 1, "label": "Start", "type": null, "kind": "CTRL", "edges": [], "proj": null}
    ],
    "groups": []
  },
  "pos": -1,
  "evt": {
    "kind": "PHASE", "peep": 0, "up": 0, "phase": "Opto",
    "node": 0, "repl": 0, "near": [], "temps": []
  }
}
```

`GraphJson.write(snap)` serializes the graph alone. `GraphJson.frame(snap, pos, evt)`
adds the playback envelope. `comp` changes for each submitted
program, and `step` starts at zero. `nodes` carries the complete graph, including
each node's `id`, `label`, `type`, `kind`, `edges`, and `proj`. Enum values use
their names; absent types, labels and projections use JSON null. Optional
`folding: true` marks nodes awaiting inline cleanup; omission means false. Missing node
references use ID zero. `pos` is the parser position, or -1 outside parsing.
`scope` identifies the parser's active scope (zero outside parsing); scope nodes
in `roots` include the parser's saved scopes. These are context references,
not IR edges.

The browser caches the parsed objects in `frames`. It computes geometry on the
first visit to a frame and keeps it in `frames[i].layout`; revisiting a frame
uses exactly that geometry. Viewport changes do not rerun layout. Nodes and
edges have stable SVG IDs within a compilation. Temporary peep geometry is
separate from these cached stable layouts.
Each frame also caches its most recent folded layout, keyed by the folded
header IDs; changing folds recomputes that view without changing the snapshot.
One `+` acknowledges the whole frame. The shared writer sends UTF-8 bytes
and supports WebSocket's 64-bit length header for frames larger than 65535 bytes.

The browser owns frame history and backward/forward playback. It
uses ELK's layered layout with fixed input ports and orthogonal routing.
Layout follows def-to-use flow downward, while the displayed arrows point
from use to def, matching Simple's edges. Control edges get a higher layout
priority; known Loop/Phi backedges get a lower priority. Expanded loops reserve
a right-side gutter for their control and Phi backedges, with separate tracks
and leads routed around node boxes. Nested loops use their own gutters; folding
a loop hides its internal backedges. Network-simplex node
placement gives forward control edges a straightness priority of 100; other
edges have zero straightness priority. Associations are drawn
as a separate overlay and do not constrain layout. Each MultiNode and its
projections occupy one box: the parent above, projection cells below in index
order. Each cell keeps its node ID, selection, highlights and edge connections;
the internal parent/projection edge is represented by the shared box. A
projection whose parent is absent is drawn on its own. Node kinds use color
and shape rather than a KIND label. Small IDs follow the name on the same line; projections use
`#id/idx`. Ordinary nodes have two lines (name and type) in a 44-pixel box.

Phi slot 0 uses a short left-facing arrow at the middle of the box's left side,
instead of a full edge to its Region. The tooltip and node details retain the
definition ID. Value inputs stay along the top.
Each Region (including a loop header) and its Phis form one layout row, with
the Region on the left. Their boxes, ports, selections and node IDs stay separate.
This also keeps a newly created Phi on its Region's row while the parser holds it.
Stops occupy the bottom of the graph, beside the Parser box when it is present,
even before return edges attach. StopCUs share a row above their outer Stop;
StartCUs sit below Start, connected by their real control inputs. Chapter adapters
identify Stops with `Kind.STOP`.
Stops and chapter 25's Start/StartCU nodes use compact single-line boxes; their
full types remain in the tooltip and selection details. Start-to-Stop SCCP
feedback edges remain in the snapshot/details but have no drawn edge or port.
Chapter 25 omits its cached zero and XCtrl nodes when only keep references hold
them alive. Actual graph uses, including Scope bindings, make them visible.
Type printers use short names for common extrema, including `MemBot`/`MemTop`,
`StructBot`/`StructTop`, `PtrBot`/`PtrTop` and `FunBot`/`FunTop`. These aliases
apply only to the exact types; more precise aliases, fields and escape sets
still print normally. Integer and float types retain their existing short names.

Scopes have named slots across the top, including control, memory and variable
bindings. Edges leave these slots upward toward their definitions. They stay visible
even when **Associations** is unchecked, and do not constrain CFG layout.
While parsing, saved scopes line up beside the graph, slightly below their
`$ctrl` definitions, with space to avoid overlapping other scopes. The active
scope stays at the bottom. The **Parser** box points to both active and saved
scopes using display-only arrows.

During parsing, values with no graph users yet appear in a separate bottom
row, with dashed display-only arrows from a **Parser**
box. Scope bindings count as users. Pending projections keep their parent box
and get an arrow to their own cell. These marks disappear when real uses attach
or parsing finishes; they do not add nodes or edges to the compiler graph.
This identifies unconsumed values from the snapshot, not Java stack references.
Nodes created inside an enclosing peephole are excluded from these Parser arrows
until that outer attempt returns; a nested constant fold can create its result
before the parent replaces the arithmetic node.

Function, loop and diamond membership comes from the compiler process; ELK
lays out the supplied hierarchy. RPO is not a pending layout requirement.
Placement can still shift substantially between rewrites.
Each visited frame currently retains a full snapshot and layout.

The viewer update task is complete as of 2026-09-26. Future considerations:

- Bounded history storage, checkpoints and deltas wait until an actual usage
  issue warrants them.
- Stable whole-graph placement awaits viewing a larger program to establish what
  is needed. Temporary neighborhoods already preserve positions across each edit.
- Compilation-unit containers remain deferred; unfinished-loop and exit-path
  grouping is adequate for current use.

### Function, loop and diamond grouping

Shared Java `GraphGroups` computes display ownership from detached snapshots.
The snapshot's `groups` array contains `{id, par, nodes}`: the header ID, parent
group ID (zero outside), and directly owned node IDs. A node has one display
home. Functions contain their CFG; natural loops nest inside functions and
other loops. Closed If/Region diamonds form nested SESE groups. Pinned values
follow their control; Phis stay with their Region. A floating node can join a
group if all semantic inputs come from that group, or all semantic outputs go
to it. Matching nominations keep it inside; sibling nominations leave it
outside both, in a common enclosing group. Nested nominations keep it in the
enclosing group: a function and its loop both nominate the function. Null slots and parser/lifetime
associations do not nominate a group, and constants can remain outside.
Ownership propagates through floating chains and is checked against the final
neighbors. Phi uses refer to the Phi itself, not its incoming CFG path: a loop
backedge value does not thereby belong to the diamond producing the backedge.
Scopes and Stops remain outside containers for their existing placement rules.

Loop membership walks backward from the backedge to the header. Before that
backedge is attached, only the header and values anchored there belong to the
loop. Paths that leave via break/return can remain in the enclosing group.
Diamond discovery checks both arms for a common Region, rejects extra CFG
entries/exits and paths that cannot reach the join, and accepts only nested or
disjoint groups. Unfinished diamonds have no box until their join closes.
This is conservative display grouping, not the compiler's loop tree.
Capture never invokes the compiler loop-tree pass, which can mutate
the graph by adding exits for infinite loops. No chapter-specific grouping
hooks or compiler edits are needed.

Expanded groups are ELK compound layout containers with a visible border and
a fold button. Sibling groups occupy disjoint boxes; nested groups remain inside
their parent. Group padding is 12 pixels at the sides/bottom and 34 at the top
for the heading and fold button. The hierarchy constrains layout and routing.

Browser-side folding replaces the group's visible contents with one thick-bordered box,
its label, a hidden-node count and an unfold button. It hides internal edges and
redirects each crossing edge to the folded box, retaining its original node IDs
and use-slot index for details and selection. Bundles share a port and route;
the tooltip retains every member, and selecting any member highlights the bundle.
Expanded node slot labels list the merged input indices. Folded boxes omit slot
labels. Unfolding restores separate edges and ports from the original snapshot.
This is a browser view of the snapshot, not a compiler graph rewrite.

Fold state persists by header ID across frames and backward playback,
including remembered child folds when a parent is unfolded. A folded group
containing a selected node or an active peephole carries that highlight.
Recompiling resets fold state along with frame history.

ELK runs locally from the pinned elkjs 0.12.0 worker; see
[`web/vendor/elk-README.md`](web/vendor/elk-README.md) for provenance and licensing.
No npm or Maven step is needed to use it.

The linearization workflow includes this directory as a shared root resource.
All chapters compile the shared Java sources from this directory; the
interactive viewer also loads the web assets from this checkout.

## Chapter adapters

The shared Java code is in `src/main/java/com/seaofnodes/graph/`:

- `GraphAdapter<N extends BaseNode<N>>` reads identity, edges, dependencies,
  labels, and types directly from the shared node contract. It constructs node
  and edge records centrally. Chapter hooks provide kinds, edge roles, scope
  binding names, projection indices, call shortcuts, and folding state as needed.
  Its final `snap` method walks definitions and uses iteratively, handles cycles,
  and sorts the copied records by ID.

Root IDs use `int[]`. Capture uses a node table indexed by ID for visitation and
root deduplication; no boxed integer lists, sets or map keys are needed.
Indexed edge access reads each chapter's native storage without copying it into
a different collection type.

Every chapter implements `com.seaofnodes.simple.print.SimpleGraphAdapter`,
extending `GraphAdapter<Node>`. The chapter owns semantic classification; the
base owns edge extraction, traversal, and snapshot assembly. The common
`BaseNode` class lives in `print/`, which is also an IDEA dependency of `graph/`.

For chapter 4:

```java
var parser = new Parser("return 1+arg+2;");
var ret = parser.parse();
var adapter = new SimpleGraphAdapter();
var frame = adapter.snap("compile-1", 0, ret, parser._scope);
```

For chapter 25:

```java
var code = new CodeGen("while(arg < 10) arg = arg + 1; return arg;").parse();
var adapter = new SimpleGraphAdapter();
var frame = adapter.snap("compile-1", 0, code._stop);
```

During a peephole, pass the current node and any unattached replacement as
additional roots. Null roots are ignored. Capture runs on the compiler thread
at a point where the graph is not changing; consumers can retain the returned
records after compilation resumes. The adapter does not compute types, invoke
peepholes, schedule nodes, or rearrange use lists.

Node IDs are the chapter's `_nid`, scoped by the caller's compilation key. Use
a new key whenever a parser/compiler resets IDs. A snapshot rejects two distinct
nodes sharing one ID. Each edge belongs to its use node and names the referenced
`def` ID and the input slot `idx` on that use: `use.in(idx) == def`. The adapter
derives these edges from the compiler's `_inputs`; `_outputs` only speeds up
traversal. Repeated definitions produce distinct edges with different slot
indices. Each input slot has an `Edge` record, including holes with `def == 0`.
Edges remain in input-slot order under their use node.
CallEnd links to known callees additionally carry `jump`, the function entry ID.
The real `def` remains the callee's Return. Chapters 18–25 supply this optional
field; zero is omitted from JSON. The browser draws a small local shortcut
beside CallEnd instead of routing that link across functions or giving it to
ELK. Clicking the shortcut (or pressing Enter/Space with it focused) centers
and selects the function at the current zoom, including a folded function.
Folding the caller retains shortcuts for external callees. Node details still
show the original input slots and Return IDs.
Projection nodes retain their own IDs and identify their parent and tuple
index, allowing the renderer to fold them into ports later. A missing parent
has `par == 0`; real node IDs are never zero.

Chapter 4 adds control/data roles and scope binding names. Chapter 25 adds
memory roles and node kinds for Phis, Regions, Loops,
Functions and compilation-unit boundaries. Shared grouping uses these existing
IR facts to compute display membership. Scope bindings and constant
lifetime edges are associations, so they need not constrain CFG layout.
Types may be absent on newly constructed nodes. Node source locations
and delta encoding remain deferred possibilities. All chapters use the observer events
described below.

Make compiles these shared sources directly into each participating chapter's
classes using `javac`. From the repository root:

```sh
make -C chapter04 build
make -C chapter25 build
```

`build` compiles the compiler and adapter without running tests or opening a
browser. Shared source changes trigger recompilation. No Maven executable,
Maven-built artifacts, generated source copies or separately installed graph
JARs are needed. Every `make view` depends on `build` through `graph/graph.mk`. Existing
`make tests` and `make release` also include the shared sources through their
normal prerequisites.

For the existing Maven/CI build path, `build-helper:add-source` describes the
same source directory. Chapter POMs select `../graph`, while the root
POM defaults to `graph` for linearized checkouts. These entries are independent
of the Make build.

## Peephole observers

`GraphObserver<N>` is a shared abstract base for `before`, `after`, `phase`, and
`dep` callbacks. `Parser._obs` (chapters 2–17) or `CodeGen._obs` (chapters 18–25) owns the
optional observer. Parser-only chapters use `Parser.PARSER` as their current
compilation context, alongside the existing global `START`. Normal compilation
leaves it null, so it does no snapshot capture or serialization. Assertions
that probe peepholes with `_midAssert` set do not generate observer calls.
Compiler hooks refer only to the shared observer base.

`GraphCapture` supplies capture and neighborhood logic. Each chapter's small
`SimpleGraphObserver` supplies its compilation context. The shared capture base
resets its state before each compilation and detaches the observer afterward.
An I/O failure disables the observer and allows optimization to continue.
The observer is detached in a `finally` block when compilation finishes or fails.

The event kinds distinguish the actual capture boundaries:

| Kind | Meaning |
| --- | --- |
| `BEFORE` | Graph before an attempt that made progress, or contained a nested rewrite |
| `RETURN` | Recursive `Node.peephole()` finished, including its local DCE; the caller has not yet attached the returned node |
| `APPLY` | A worklist attempt finished after substitution, dependency draining, and unused-node cleanup |
| `PHASE` | Parse, Iter, Opto, or TypeCheck completed, including work outside individual peepholes |
| `ERROR` | TypeCheck failed; `node` identifies the offending node (zero if unavailable), and `msg` contains the diagnostic |

`peep` identifies an attempt; `up` identifies its enclosing attempt, or zero.
`node` and `repl` identify the original and result nodes. A zero `repl` on
`APPLY` means removal without a surviving result. Phase events use zero IDs.
An in-place rewrite has equal `node` and `repl` IDs. Type changes also count as
progress; these events are not restricted to structural rewrites.

Before snapshots are buffered until the outcome is known. Attempts without
progress are discarded unless they contain a visible child rewrite. Ancestor
before frames are emitted before their children; after frames close the nested
attempts. Frame steps are assigned consecutively on emission, while attempt IDs
may have gaps. A failed compilation can leave an unfinished attempt; the next
compilation starts with empty capture state and a new compilation key.

Capture includes the active attempts and returned node as extra roots, so nodes
not yet attached by the parser remain visible. Dead originals are not added as
after-frame roots. The observer never adds keep edges, changes use lists, or
modifies the compiler's dependency tracking.

`temps` lists nodes created since the outermost active peep's before snapshot,
while a nested peep is being shown. They have not returned to the parser yet.
The list is empty on outermost results and phase events. This distinguishes
userless peep temporaries from parser-held expression values without adding
compiler edges or inferring ownership from the current result node alone.

`near` is a primitive array of node IDs, assembled with a `BitSet`. It combines:

- The current node and replacement, with their immediate defs and uses before
  and after the rewrite.
- Existing wake-up dependents, read before those lists are drained.
- Nodes named by `addDep`/`addDepForwards` during the attempt, including calls
  whose dependency already exists or is an immediate neighbor.
- Neighborhoods of nested attempts.

This is initial display context, not an exact record of every node read or
changed by a rule. Dependency lists contain wake-up dependents; they do not
describe an entire successful pattern match. Some rules inspect distant nodes
without registering a dependency. Before events include context known when
emitted; their matching after events can add more. IDs of deleted nodes remain
in `near`, and the browser highlights only those present in the current frame.
The animation uses graph diffs to include every changed node in the enclosure.

The browser keeps the event alongside its snapshot and layout. It labels and
highlights the current step and gathers each enclosing rewrite's neighborhood.
Nested captures remain available in the stream, but playback presents their net
effect as part of the enclosing edit. Chapter 4 uses the same event model, with
defs/uses for its neighborhoods and no worklist or dependency lists. It retains the final Return as a root after scope cleanup.

The compiler DOT dump flag, parser graph directive, Java DOT generators and old
viewer/transport classes have been removed from all chapters. Use **Save SVG**
for new documentation figures. The documentation Make rules render maintained
JSON snapshots with the shared viewer and the remaining `.gv` figures with DOT.

Chapter 1 has no peepholes and supplies one completed parse snapshot. Chapter 2
shows constant folding; chapters 3 and 4 add variables and algebraic rewrites.
Chapters 9–17 also capture worklist application and dependency neighborhoods;
chapters 18–25 use their CodeGen phase/context. Each chapter retains only its
`SimpleGraphAdapter`, `SimpleGraphObserver`, an optional observer field and the
compiler callbacks/accessors needed by those two classes.