summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java24
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java30
-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
5 files changed, 179 insertions, 33 deletions
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<ScopeNode> xScopes = midParse ? CODE.P._xScopes : null;
+ var roots = new ArrayList<Node>();
+ 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<Node> 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<len; i++ )
_str[i] = (byte)(rawget() ^ _key[i & 3]);
- return new String(_str,0,len);
+ return new String(_str,0,len,StandardCharsets.UTF_8);
}
// Client closes
case 8: return null;
@@ -155,24 +155,24 @@ class SimpleWebSocket extends ServerSocket {
}
}
- // Classic put to browser client. No encoding.
+ // Server text frames are UTF-8 and unmasked.
public void put(String msg) throws IOException {
- int len = msg.length();
+ byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
+ int len = bytes.length;
+ _out.write(129); // FIN, opcode 1 - whole text message
if( len <= 125 ) {
- _out.write(129); // FIN, opcode 1 - whole text message
_out.write(len);
- for( int i=0; i<msg.length(); i++ )
- _out.write(msg.charAt(i));
- return;
+ } else if( len <= 65535 ) {
+ _out.write(126);
+ _out.write(len>>>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<msg.length(); i++ )
- _out.write(msg.charAt(i));
+ _out.write(bytes);
+ _out.flush();
}
@Override public void close() throws IOException {
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);