summaryrefslogtreecommitdiff
path: root/chapter25
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-09-22 09:32:07 -0700
committerCliff Click <cliffc@acm.org>2026-09-22 09:32:07 -0700
commit1d961838cff168a101069d08a8ab6594f49f0911 (patch)
treef832576181cc396c033707508a50e3d72120b8c6 /chapter25
parent92e3013a4cab12747af9d5484e7caad6fc454b1e (diff)
downloadSimple-1d961838cff168a101069d08a8ab6594f49f0911.tar.gz
Simple-1d961838cff168a101069d08a8ab6594f49f0911.zip
Graph viewer on ch4 and25
Diffstat (limited to 'chapter25')
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java68
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java3
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java124
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java193
4 files changed, 18 insertions, 370 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 10c07730..03842119 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/print/JSViewer.java
@@ -1,72 +1,20 @@
package com.seaofnodes.simple.print;
-import com.seaofnodes.graph.GraphJson;
-import com.seaofnodes.graph.GraphEvent;
-import com.seaofnodes.graph.GraphSnapshot;
+import com.seaofnodes.graph.*;
import com.seaofnodes.simple.codegen.CodeGen;
import java.io.IOException;
-import java.nio.file.Paths;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.net.URI;
-public class JSViewer implements AutoCloseable {
- // Display programs in an endless loop
- public static void main( String[] args ) throws Exception {
- try(var js = new JSViewer()) {
- js.run();
- }
- }
-
- // WebSocket to the browser for display
- private final SimpleWebSocket _server;
-
- JSViewer() throws Exception {
- // Launch server; handshake
- _server = new SimpleWebSocket(viewerURI(),12345);
- }
-
- // The browser application is shared by all chapters. Search upwards so
- // chapter, repository-root, and linearized checkouts use the same assets.
- static URI viewerURI() throws IOException {
- String url = System.getProperty("simple.graph.url");
- if( url != null ) return URI.create(url);
- for( Path dir = Paths.get("").toAbsolutePath(); dir != null; dir = dir.getParent() ) {
- Path page = dir.resolve("graph/web/index.html");
- if( Files.isRegularFile(page) ) return page.toUri();
- }
- throw new IOException("Cannot find graph/web/index.html; run inside the Simple checkout "
- + "or set -Dsimple.graph.url=<viewer URL>");
- }
-
- void run( ) throws Exception {
- _server.put("!");
- while( true ) {
- String src = _server.get();
- switch( src ) {
- case null: return;
- case "null": return;
- case "+": break; // Client requests more frames, but we send them all anyway
- default:
- System.out.println(src);
- try {
- compile(src);
- // Catch and ignore Parser errors
- } catch(RuntimeException re) {
- System.err.println(re);
- } finally {
- _server.put("#"); // Final frame
- }
- break;
- }
- }
+public class JSViewer extends GraphViewer {
+ public static void main(String[] args) throws IOException {
+ try( var view = new JSViewer() ) { view.run(); }
}
+ JSViewer() throws IOException {}
- private void compile(String src) {
+ @Override protected void compile(String src) {
CodeGen code = new CodeGen(src);
code._obs = new SimpleGraphObserver(code) {
@Override protected void frame(GraphSnapshot snap, int pos, GraphEvent evt) throws IOException {
- _server.put(GraphJson.frame(snap, pos, evt));
+ JSViewer.this.frame(snap, pos, evt);
}
};
try {
@@ -75,6 +23,4 @@ public class JSViewer implements AutoCloseable {
code._obs = null;
}
}
-
- @Override public void close() throws IOException { _server.close(); }
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java
index 592abb5c..e9b1dc6d 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java
@@ -10,6 +10,9 @@ import java.util.ArrayList;
/** Chapter 25's view of the IR, including partially constructed parse-time graphs. */
public class SimpleGraphAdapter extends GraphAdapter<Node> {
+ @Override protected boolean dead(Node n) { return n.isDead(); }
+ @Override protected int nDeps(Node n) { return n.nDeps(); }
+ @Override protected Node dep(Node n, int idx) { return n.dep(idx); }
@Override protected int id(Node n) { return n._nid; }
@Override protected int nIns(Node n) { return n.nIns(); }
@Override protected Node in(Node n, int idx) { return n.in(idx); }
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 5d511287..0318b793 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -1,130 +1,22 @@
package com.seaofnodes.simple.print;
-import com.seaofnodes.graph.GraphEvent;
-import com.seaofnodes.graph.GraphObserver;
-import com.seaofnodes.graph.GraphSnapshot;
+import com.seaofnodes.graph.GraphCapture;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.node.Node;
-import java.io.IOException;
import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.UUID;
-/** Capture events and neighborhoods on the compiler thread. Transport is supplied by the viewer. */
-public abstract class SimpleGraphObserver extends GraphObserver<Node> {
+/** Chapter 25's compilation context; capture and event bookkeeping are shared. */
+public abstract class SimpleGraphObserver extends GraphCapture<Node> {
private final CodeGen _code;
- private final SimpleGraphAdapter _graph = new SimpleGraphAdapter();
- private final String _comp = UUID.randomUUID().toString();
- private final ArrayList<Peep> _peeps = new ArrayList<>();
- private long _next, _step;
- private boolean _off;
-
- protected SimpleGraphObserver(CodeGen code) { _code = code; }
-
- protected abstract void frame(GraphSnapshot snap, int pos, GraphEvent evt) throws IOException;
-
- private static class Peep {
- final Node node;
- final long id, up;
- final String phase;
- final int pos;
- final BitSet near = new BitSet();
- GraphSnapshot pre;
- boolean sent;
-
- Peep(Node n, long id, long up, String phase, int pos) {
- node = n; this.id = id; this.up = up; this.phase = phase; this.pos = pos;
- }
- }
-
- @Override public void before(Node n) {
- if( _off ) return;
- var p = new Peep(n, ++_next, _peeps.isEmpty() ? 0 : _peeps.getLast().id,
- _code._phase.name(), pos());
- _peeps.add(p);
- near(p, n);
- // Buffer until we know that this attempt (or a nested one) made progress.
- // Include n explicitly: parse-time nodes need not have any uses yet.
- p.pre = snap(n);
- }
-
- @Override public void after(Node n, Node repl, boolean applied) {
- if( _off ) return;
- Peep p = _peeps.getLast();
- assert p.node == n;
- near(p, n);
- near(p, repl);
- for( Peep up : _peeps ) up.near.or(p.near);
- if( repl != null || p.sent ) {
- // Emit ancestors first so a nested rewrite always has its context.
- for( Peep up : _peeps ) {
- if( up.sent ) continue;
- emit(up.pre, up.pos, evt(up, GraphEvent.Kind.BEFORE, 0));
- if( _off ) return;
- up.pre = null;
- up.sent = true;
- }
- Node rez = repl == null ? n : repl;
- emit(snap(rez.isDead() ? null : rez), pos(),
- evt(p, applied ? GraphEvent.Kind.APPLY : GraphEvent.Kind.RETURN,
- rez.isDead() ? 0 : rez._nid));
- }
- _peeps.removeLast();
- }
-
- @Override public void dep(Node n, Node def) {
- if( _off || _peeps.isEmpty() ) return;
- Peep p = _peeps.getLast();
- add(p, n);
- add(p, def);
- }
-
- @Override public void phase(String phase) {
- if( _off ) return;
- emit(snap(null), pos(), new GraphEvent(GraphEvent.Kind.PHASE, 0, 0, phase, 0, 0, new int[0]));
- }
-
- private GraphEvent evt(Peep p, GraphEvent.Kind kind, int repl) {
- return new GraphEvent(kind, p.id, p.up, p.phase, p.node._nid, repl, p.near.stream().toArray());
- }
-
- private void add(Peep p, Node n) { if( n != null ) p.near.set(n._nid); }
-
- private void near(Peep p, Node n) {
- if( n == null ) return;
- add(p, n);
- for( Node def : n._inputs ) add(p, def);
- for( Node use : n._outputs ) add(p, use);
- // These are wake-up dependents, not all nodes inspected by a rule.
- // Read before the compiler drains the list; never move/clear it here.
- for( int i = 0; i < n.nDeps(); i++ ) add(p, n.dep(i));
- }
-
- private int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; }
-
- private GraphSnapshot snap(Node extra) {
- var roots = new ArrayList<Node>();
+ protected SimpleGraphObserver(CodeGen code) { super(new SimpleGraphAdapter()); _code = code; }
+ @Override protected String phase() { return _code._phase.name(); }
+ @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; }
+ @Override protected void detach() { _code._obs = null; }
+ @Override protected void roots(ArrayList<Node> roots) {
roots.add(_code._stop);
if( _code._phase == CodeGen.Phase.Parse ) {
roots.add(_code.P._scope);
roots.addAll(_code.P._xScopes);
}
- for( Peep p : _peeps ) if( !p.node.isDead() ) roots.add(p.node);
- roots.add(extra);
- return _graph.snap(_comp, 0, roots.toArray(Node[]::new));
- }
-
- private void emit(GraphSnapshot snap, int pos, GraphEvent evt) {
- if( _off ) return;
- // Assign steps on emission; attempts without progress do not consume frames.
- snap = new GraphSnapshot(snap.ver(), _comp, _step++, snap.roots(), snap.nodes());
- try {
- frame(snap, pos, evt);
- } catch( IOException e ) {
- // A disconnected display must not interrupt optimization.
- _off = true;
- _code._obs = null;
- System.err.println("Graph viewer disconnected: " + e.getMessage());
- }
}
}
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java
deleted file mode 100644
index ae500879..00000000
--- a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleWebSocket.java
+++ /dev/null
@@ -1,193 +0,0 @@
-package com.seaofnodes.simple.print;
-
-import com.seaofnodes.simple.util.Utils;
-import java.io.*;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Base64;
-import java.net.InetSocketAddress;
-import java.nio.file.Path;
-import com.sun.net.httpserver.HttpServer;
-import com.sun.net.httpserver.SimpleFileServer;
-
-// Why-Oh-Why is this needed?
-
-// Pass in a URI and a port.
-// Opens the client; client has to open a websocket at the given port.
-// Connects, handshakes.
-// Thereafter you can call get/put to swap frames with client.
-
-class SimpleWebSocket extends ServerSocket {
- final InputStream _in;
- final OutputStream _out;
- private HttpServer _http;
- private Socket _client;
-
- SimpleWebSocket(URI uri, int port) throws IOException, NoSuchAlgorithmException {
- super(port);
- try {
- // Use an ordinary web URL; desktop dispatch of file: URLs can silently
- // raise the browser without opening the page.
- if( "file".equals(uri.getScheme()) ) {
- Path page = Path.of(uri);
- _http = SimpleFileServer.createFileServer(
- new InetSocketAddress("127.0.0.1",0), page.getParent(),
- SimpleFileServer.OutputLevel.NONE);
- _http.start();
- uri = URI.create("http://127.0.0.1:" + _http.getAddress().getPort()
- + "/" + page.getFileName());
- }
- // Print the address even if the desktop silently fails to open a tab.
- System.out.println("Graph viewer: " + uri);
- if( Boolean.parseBoolean(System.getProperty("simple.graph.open", "true")) )
- java.awt.Desktop.getDesktop().browse(uri);
- // Look for client; get in/out streams
- Socket sock = accept();
- _client = sock;
- _in = sock. getInputStream();
- _out = sock.getOutputStream();
-
-
- // Establish web socket with handshake; easier with a buffered reader
- BufferedReader br = new BufferedReader(new InputStreamReader(_in, StandardCharsets.US_ASCII));
- String key = null;
- // TCP can deliver the request in several chunks. ready() is not an
- // end-of-headers test; wait for the terminating empty line.
- String line;
- while( (line = br.readLine()) != null && !line.isEmpty() ) {
- if( line.startsWith("Sec-WebSocket-Key: ") )
- key = line.substring(19);
- }
- if( key==null || line==null ) {
- sock.close();
- super.close();
- throw new IOException("Incomplete WebSocket upgrade request");
- }
- // Magic handshake
- byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n"
- + "Connection: Upgrade\r\n"
- + "Upgrade: websocket\r\n"
- + "Sec-WebSocket-Accept: "
- + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-1").digest((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes( StandardCharsets.UTF_8 )))
- + "\r\n\r\n").getBytes( StandardCharsets.UTF_8 );
- _out.write(response,0,response.length);
- _out.flush();
- } catch( IOException | NoSuchAlgorithmException | RuntimeException | Error failure ) {
- if( _http != null ) _http.stop(0);
- if( _client != null ) {
- try { _client.close(); } catch( IOException ignored ) {}
- }
- try { super.close(); } catch( IOException ignored ) {}
- throw failure;
- }
- }
-
- final byte[] _ins = new byte[1024];
- final byte[] _str = new byte[1024];
- final byte[] _key = new byte[4];
- int _x, _len;
- private int rawget() { assert _x<_len; return _ins[_x++]&0xFF; }
-
- // TODO: Flip to Direct Byte Buffer, this is just a poor-mans version
- private void flipRead() throws IOException {
- // Move _x to _len to offset 0
- System.arraycopy(_ins,_x,_ins,0,(_len-_x));
- _len = _len-_x;
- _x=0;
- // Read more
- int len = _in.read(_ins,_len,_ins.length-_len);
- if( len == -1 )
- throw Utils.TODO(); // EOF
- _len += len;
- }
- // Block until you read at least len
- private void atLeast(int len) throws IOException {
- if( _x+len <= _len ) return;
- flipRead();
- }
-
-
- // Classic read from browser client. Encoded, so decoded here
- public String get() throws IOException {
- atLeast(1); // Need 1 byte for opcode
- int op = rawget();
- if( (op&0x80)!=0x80 ) // FIN bit not set?
- throw Utils.TODO( "Multi-part from client: " + op ); // Multipart message
- op &= 0x7F; // Strip FIN bit
- switch( op&15) {
- case 1: {
- atLeast(5); // Min message length
- // Message length
- int len = rawget()-128;
- if( len > 125 ) {
- if( len > 126 )
- throw Utils.TODO(); // Multi-byte length
- len = (rawget()<<8) | rawget();
- }
- atLeast(len+4); // Rest of message
- // Encoding key
- _key[0] = (byte)rawget();
- _key[1] = (byte)rawget();
- _key[2] = (byte)rawget();
- _key[3] = (byte)rawget();
-
- // Decode bytes
- for( int i=0; i<len; i++ )
- _str[i] = (byte)(rawget() ^ _key[i & 3]);
-
- return new String(_str,0,len,StandardCharsets.UTF_8);
- }
- // Client closes
- case 8: return null;
- // Ping
- case 9:
- // Send a PONG back. TODO: echo PING data in the PONG
- _out.write(0x8A); // FIN + PONG
- _out.write(0x00); // LENGTH==0
- return get();
- // Not (yet) implemented: continuation frames, ping/pong
- default:
- throw Utils.TODO("Unexpected op from client: "+op); // Multi-part message
- }
- }
-
- // Server text frames are UTF-8 and unmasked.
- public void put(String msg) throws IOException {
- 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(len);
- } 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);
- }
- _out.write(bytes);
- _out.flush();
- }
-
- @Override public void close() throws IOException {
- try {
- if( _out != null ) {
- _out.write(new byte[]{(byte)0x88, 0}); // Complete, empty close frame
- _out.flush();
- }
- } finally {
- if( _http != null ) _http.stop(0);
- try {
- if( _client != null ) _client.close();
- } finally {
- super.close();
- }
- }
- }
-}