summaryrefslogtreecommitdiff
path: root/graph
diff options
context:
space:
mode:
Diffstat (limited to 'graph')
-rw-r--r--graph/README.md47
-rw-r--r--graph/src/main/java/com/seaofnodes/graph/GraphJson.java87
-rw-r--r--graph/web/viewer.js24
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);