diff options
| author | Cliff Click <cliffc@acm.org> | 2026-09-21 21:26:03 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-09-21 21:26:03 -0700 |
| commit | e587a11049129866bf3fd6db0a0f72d44ad6b39f (patch) | |
| tree | 6282cc18061dfe9e2835847b535794137f047a2c /graph | |
| parent | 21b9bdf9da645a9b392db0e7ec132fb9aeed5339 (diff) | |
| download | Simple-e587a11049129866bf3fd6db0a0f72d44ad6b39f.tar.gz Simple-e587a11049129866bf3fd6db0a0f72d44ad6b39f.zip | |
Change protocol between compiler and browser
Diffstat (limited to 'graph')
| -rw-r--r-- | graph/README.md | 47 | ||||
| -rw-r--r-- | graph/src/main/java/com/seaofnodes/graph/GraphJson.java | 87 | ||||
| -rw-r--r-- | graph/web/viewer.js | 24 |
3 files changed, 145 insertions, 13 deletions
diff --git a/graph/README.md b/graph/README.md index caf18f70..e942e055 100644 --- a/graph/README.md +++ b/graph/README.md @@ -12,8 +12,10 @@ 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 retains the existing DOT/WebSocket protocol. No JavaScript package -installation, frontend build, or separate server process is required. +The viewer uses the existing WebSocket connection. Chapter 25 sends structured +JSON snapshots alongside DOT in each frame; earlier chapters send bare DOT. +No JavaScript package installation, frontend build, or separate server process +is required. For development over HTTP, run this from the repository root: ```sh @@ -51,22 +53,51 @@ WebSocket transport. The browser code has no dependency on a chapter's Java classes. The adapters below share snapshot capture; moving the transport into the shared Java code is a separate step. -The legacy protocol is: +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 | `digraph ...` | One complete DOT snapshot | +| Compiler to browser | JSON `{snap, pos, dot}` | Chapter 25's complete snapshot and current drawing | | Browser to compiler | `+` | Acknowledge/request another frame | | Compiler to browser | `#` | End of sequence | | Browser to compiler | `null` | End the session | -DOT frames may contain a `// POS:` comment identifying the parser position. -The browser owns frame history and backward/forward playback. +Chapter 25's JSON frame has this shape: + +```json +{ + "snap": { + "ver": 1, "comp": "compilation UUID", "step": 0, "roots": [1], + "nodes": [ + {"id": 1, "label": "Start", "type": null, "kind": "CTRL", "edges": [], "proj": null} + ] + }, + "pos": -1, + "dot": "digraph view_0 { ... }" +} +``` + +`GraphJson.write(snap)` serializes the graph alone. `GraphJson.frame(snap, pos, +dot)` adds the temporary 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. Missing node +references use ID zero. `pos` is the parser position, or -1 outside parsing. + +The browser caches the parsed objects in `frames`; `frames[i].snap` is available +for inspecting the new model. It still renders `frames[i].dot`. This temporarily +sends both representations; the ELK step will remove the DOT portion. One `+` +acknowledges the whole frame. The chapter 25 writer sends UTF-8 bytes and supports +WebSocket's 64-bit length header for frames larger than 65535 bytes. + +Earlier chapters' DOT frames may contain a `// POS:` comment identifying the +parser position. The browser owns frame history and backward/forward playback. -The next layout implementation belongs here. The compiler-side adapter below -is its first building block; the running viewer still consumes DOT. +The next layout implementation belongs here. The compiler-side adapter and +JSON boundary are in place; the running renderer still consumes DOT. The linearization workflow includes this directory as a shared root resource. Chapters 4 and 25 compile the shared Java sources from this directory; the @@ -135,7 +166,7 @@ Functions and compilation-unit boundaries. These are existing IR facts, not computed region membership or a SESE hierarchy. Scope bindings and constant lifetime edges are associations, so they need not constrain CFG layout. Types may be absent on newly constructed nodes. Source locations, grouping, -observer events, serialization and delta encoding remain follow-up work. +observer events and delta encoding remain follow-up work. Make compiles these shared sources directly into each participating chapter's classes using `javac`. From the repository root: diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphJson.java b/graph/src/main/java/com/seaofnodes/graph/GraphJson.java new file mode 100644 index 00000000..a63a0143 --- /dev/null +++ b/graph/src/main/java/com/seaofnodes/graph/GraphJson.java @@ -0,0 +1,87 @@ +package com.seaofnodes.graph; + +/** JSON for the shared graph model; no chapter or layout dependencies. */ +public class GraphJson { + public static String write(GraphSnapshot snap) { + var sb = new StringBuilder(); + snap(sb, snap); + return sb.toString(); + } + + // Temporary envelope while the browser still renders DOT. The snapshot is + // complete on its own; step 2 can remove dot without changing its schema. + public static String frame(GraphSnapshot snap, int pos, String dot) { + var sb = new StringBuilder("{\"snap\":"); + snap(sb, snap); + sb.append(",\"pos\":").append(pos).append(",\"dot\":"); + str(sb, dot); + return sb.append('}').toString(); + } + + private static void snap(StringBuilder sb, GraphSnapshot snap) { + sb.append("{\"ver\":").append(snap.ver()).append(",\"comp\":"); + str(sb, snap.comp()); + sb.append(",\"step\":").append(snap.step()).append(",\"roots\":["); + int[] roots = snap.roots(); + for( int i = 0; i < roots.length; i++ ) { + if( i > 0 ) sb.append(','); + sb.append(roots[i]); + } + sb.append("],\"nodes\":["); + var nodes = snap.nodes(); + for( int i = 0; i < nodes.size(); i++ ) { + if( i > 0 ) sb.append(','); + var node = nodes.get(i); + sb.append("{\"id\":").append(node.id()).append(",\"label\":"); + str(sb, node.label()); + sb.append(",\"type\":"); + str(sb, node.type()); + sb.append(",\"kind\":"); + str(sb, node.kind().name()); + sb.append(",\"edges\":["); + var edges = node.edges(); + for( int j = 0; j < edges.size(); j++ ) { + if( j > 0 ) sb.append(','); + var edge = edges.get(j); + sb.append("{\"idx\":").append(edge.idx()) + .append(",\"def\":").append(edge.def()).append(",\"role\":"); + str(sb, edge.role().name()); + sb.append(",\"label\":"); + str(sb, edge.label()); + sb.append('}'); + } + sb.append("],\"proj\":"); + var proj = node.proj(); + if( proj == null ) sb.append("null"); + else sb.append("{\"par\":").append(proj.par()) + .append(",\"idx\":").append(proj.idx()).append('}'); + sb.append('}'); + } + sb.append("]}"); + } + + private static void str(StringBuilder sb, String s) { + if( s == null ) { sb.append("null"); return; } + sb.append('"'); + for( int i = 0; i < s.length(); i++ ) { + char c = s.charAt(i); + switch( c ) { + case '"' -> sb.append("\\\""); + case '\\' -> sb.append("\\\\"); + case '\n' -> sb.append("\\n"); + case '\r' -> sb.append("\\r"); + case '\t' -> sb.append("\\t"); + default -> { + // Escape controls and UTF-16 code units that cannot travel alone + // in UTF-8. Surrogate pairs round-trip through JSON as well. + if( c < 32 || Character.isSurrogate(c) ) { + sb.append("\\u"); + for( int shift = 12; shift >= 0; shift -= 4 ) + sb.append("0123456789abcdef".charAt((c >> shift) & 15)); + } else sb.append(c); + } + } + } + sb.append('"'); + } +} diff --git a/graph/web/viewer.js b/graph/web/viewer.js index 3234617c..e052e141 100644 --- a/graph/web/viewer.js +++ b/graph/web/viewer.js @@ -60,10 +60,10 @@ function render(index) { const frameGeneration = generation; rendering = true; updateUI(); - const position = frames[index].match(/\/\/ POS:\s*(\d+)/); - if (position) program.setSelectionRange(Number(position[1]), Number(position[1]) + 1); + const frame = frames[index]; + if (frame.pos >= 0) program.setSelectionRange(frame.pos, frame.pos + 1); try { - renderer.renderDot(frames[index], () => { + renderer.renderDot(frame.dot, () => { rendering = false; if (generation === frameGeneration) current = index; updateUI(); @@ -102,8 +102,22 @@ try { done = true; compiling = false; updateUI(); - } else if (message.startsWith("digraph")) { - frames.push(message); + } else if (message.startsWith("{") || message.startsWith("digraph")) { + let frame; + try { + if (message.startsWith("{")) { + frame = JSON.parse(message); + if (frame.snap.ver !== 1) throw new Error("Unsupported graph version: " + frame.snap.ver); + } else { + // Earlier chapters still send bare DOT. + const pos = message.match(/\/\/ POS:\s*(\d+)/); + frame = {dot: message, pos: pos ? Number(pos[1]) : -1}; + } + } catch (error) { + reportError(error); + return; + } + frames.push(frame); socket.send("+"); updateUI(); if (frames.length === 1) render(0); |
