summaryrefslogtreecommitdiff
path: root/graph/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'graph/README.md')
-rw-r--r--graph/README.md581
1 files changed, 581 insertions, 0 deletions
diff --git a/graph/README.md b/graph/README.md
new file mode 100644
index 00000000..8577f509
--- /dev/null
+++ b/graph/README.md
@@ -0,0 +1,581 @@
+# 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 viewer starts idle. Enter a program and 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 10-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 10's
+[load-after-store example](../chapter10/README.md#local-memory-optimizations)
+uses this format to isolate a single rewrite.
+
+Choose how much control to show according to the lesson. Keep it when teaching
+control construction, branch elimination, loop nesting, or `break`/`continue`.
+For a value rewrite inside an otherwise obvious branch or loop, omit the
+routine CFG and its predicate-only computations, while preserving Phi data
+inputs and their order. Chapter 5's arithmetic/Phi examples and Chapter 9's
+optimization examples use this convention. Detached True/False projections
+can identify important gates, including the two Returns in Chapter 9's first
+GVN example. Describe omitted context in the caption or surrounding text.
+If a snapshot omits a Loop, retain `"backedge": true` on its Phis' backedge
+inputs so layout still puts the loop header values above their updates. This
+is a documentation layout hint; live snapshots derive it from the Loop node.
+
+The memory diagrams in Chapters 10, 11, and 13 also use viewer snapshots, with definitions
+above uses (apart from loop backedges) and arrows from use to definition.
+They omit control-flow context, including Regions and their Phi bindings,
+while retaining data and memory dependencies. Small, detached True/False
+projections show branch gates for Loads and Stores. Chapter 10 shows schematic
+source gates; Chapter 13 shows actual bindings at each scheduling stage, with
+entry-block and Loop bindings omitted. Its infinite-loop figures retain the
+CFG because that is their subject. Empty control slots in focused snapshots
+can denote omitted context rather than null inputs in the compiler.
+Type lattices retain DOT sources, their own
+domain colors, and top-to-bottom lattice order, rather than IR edge conventions.
+
+Routed edges keep their horizontal and vertical segments, with rounded corners
+in both the live viewer and SVG exports. The bend radius shrinks on short
+segments so adjacent corners do not overlap.
+
+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.