From e587a11049129866bf3fd6db0a0f72d44ad6b39f Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Mon, 21 Sep 2026 21:26:03 -0700 Subject: Change protocol between compiler and browser --- .../java/com/seaofnodes/simple/print/JSViewer.java | 24 ++++-- .../seaofnodes/simple/print/SimpleWebSocket.java | 30 ++++---- graph/README.md | 47 ++++++++++-- .../main/java/com/seaofnodes/graph/GraphJson.java | 87 ++++++++++++++++++++++ graph/web/viewer.js | 24 ++++-- 5 files changed, 179 insertions(+), 33 deletions(-) create mode 100644 graph/src/main/java/com/seaofnodes/graph/GraphJson.java diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java b/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java index 156f1fe9..a9f6bfd4 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple.print; +import com.seaofnodes.graph.GraphJson; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.*; @@ -11,8 +12,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.net.URI; import java.util.Collection; +import java.util.ArrayList; import java.util.Arrays; import java.util.Stack; +import java.util.UUID; import static com.seaofnodes.simple.codegen.CodeGen.CODE; public class JSViewer implements AutoCloseable { @@ -28,7 +31,9 @@ public class JSViewer implements AutoCloseable { // WebSocket to the browser for display static SimpleWebSocket SERVER; - static int N; // Dot frames + static long N; // Snapshot step within this compilation + static String COMP; + static final SimpleGraphAdapter GRAPH = new SimpleGraphAdapter(); JSViewer() throws Exception { // Launch server; handshake @@ -60,6 +65,7 @@ public class JSViewer implements AutoCloseable { System.out.println(src); try { N=0; + COMP = UUID.randomUUID().toString(); // Use parser scope, xscope when building views CodeGen code = new CodeGen(src); SHOW = true; @@ -93,10 +99,19 @@ public class JSViewer implements AutoCloseable { return; boolean midParse = CODE._phase == CodeGen.Phase.Parse; Stack xScopes = midParse ? CODE.P._xScopes : null; + var roots = new ArrayList(); + roots.add(CODE._stop); + if( midParse ) { + roots.add(CODE.P._scope); + roots.addAll(xScopes); + } + long step = N++; + var snap = GRAPH.snap(COMP, step, roots.toArray(Node[]::new)); + int pos = midParse ? CODE.P.pos() : -1; Collection all = GraphVisualizer.findAll(xScopes, CODE._stop, midParse ? CODE.P._scope: null); SB sb = new SB(); - sb.p("digraph view_").p(N++).p(" {\n").ii(); + sb.p("digraph view_").p(step).p(" {\n").ii(); if( midParse ) sb.i().p("// POS: ").p(CODE.P.pos()).nl(); @@ -139,11 +154,10 @@ public class JSViewer implements AutoCloseable { sb.p("}\n").di(); - // Tell client another DOT frame + // Send the model and its current DOT rendering as one playback frame. String dot = sb.toString(); - System.out.println(dot); try { - SERVER.put(dot); + SERVER.put(GraphJson.frame(snap, pos, dot)); } catch( IOException ioe ) { try { SERVER.close();} catch( IOException ignored ){} } diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java index d7fa8bf2..ae500879 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java @@ -139,7 +139,7 @@ class SimpleWebSocket extends ServerSocket { for( int i=0; i>>8); + _out.write(len&255); + } else { + _out.write(127); + for( int shift = 56; shift >= 0; shift -= 8 ) + _out.write((int)((long)len >>> shift) & 255); } - if( len > 65535 ) - throw Utils.TODO("MOAR LENGTH"); - _out.write(129); // FIN, opcode 1 - whole text message - _out.write(126); // Length is 16b - _out.write(len>>>8); - _out.write(len&255); - for( int i=0; i` 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); -- cgit v1.2.3