summaryrefslogtreecommitdiff
path: root/src/main/java/com/seaofnodes/simple/JSViewer.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/seaofnodes/simple/JSViewer.java')
-rw-r--r--src/main/java/com/seaofnodes/simple/JSViewer.java394
1 files changed, 394 insertions, 0 deletions
diff --git a/src/main/java/com/seaofnodes/simple/JSViewer.java b/src/main/java/com/seaofnodes/simple/JSViewer.java
new file mode 100644
index 00000000..7422767a
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/JSViewer.java
@@ -0,0 +1,394 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import java.io.IOException;
+import java.util.Stack;
+import java.nio.file.Paths;
+import java.util.Collection;
+
+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();
+ }
+ }
+
+ static boolean SHOW;
+
+ // WebSocket to the browser for display
+ static SimpleWebSocket SERVER;
+
+ static int N; // Dot frames
+ static CodeGen CODE; //
+
+ JSViewer() throws Exception {
+ // Launch server; handshake
+ SERVER = new SimpleWebSocket(Paths.get("docs/index.html").toUri(),12345) ;
+ }
+
+ 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 {
+ N=0;
+ // Use parser scope, xscope when building views
+ CODE = new CodeGen(src);
+ SHOW = true;
+ show();
+ // Parse program, generating views at every parse point
+ CODE.parse();
+ // No longer user parse internal state when building views
+ CODE.opto();
+
+ // Catch and ignore Parser errors
+ } catch(RuntimeException re) {
+ System.err.println(re);
+ } finally {
+ SHOW = false;
+ SERVER.put("#"); // Final frame
+ }
+ break;
+ }
+ }
+ }
+
+ @Override public void close() throws IOException {
+ SERVER.close();
+ SERVER=null;
+ }
+
+ public static void show() { if( SERVER!=null && SHOW ) _show(); }
+ private static void _show() {
+ boolean midParse = CODE.P!=null;
+ // Skip util we at least get the Parse object made
+ if( !midParse && (CODE._phase==null || CODE._phase == CodeGen.Phase.Parse) )
+ return;
+ Stack<ScopeNode> xScopes = midParse ? CODE.P._xScopes : null;
+
+ 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();
+ if( midParse )
+ sb.i().p("// POS: ").p(CODE.P.pos()).nl();
+
+ // To keep the Scopes below the graph and pointing up into the graph we
+ // need to group the Nodes in a subgraph cluster, and the scopes into a
+ // different subgraph cluster. THEN we can draw edges between the
+ // scopes and nodes. If we try to cross subgraph cluster borders while
+ // still making the subgraphs DOT gets confused.
+ sb.i().p("rankdir=BT;\n"); // Force Nodes before Scopes
+
+ // CNC - turned off Apr/8/2024, gives more flex in the layout and
+ // removes some of the more ludicrous layout choices.
+ // Preserve node input order
+ //sb.append("ordering=\"in\";\n");
+
+ // Merge multiple edges hitting the same node. Makes common shared
+ // nodes much prettier to look at.
+ sb.i().p("concentrate=\"true\";\n");
+
+ // Force nested scopes to order
+ sb.i().p("compound=\"true\";\n");
+
+ // Just the Nodes first, in a cluster no edges
+ nodes(sb, all);
+
+ // Now the scopes, in a cluster no edges
+ if( midParse )
+ for( ScopeNode xscope : xScopes )
+ if( !xscope.isDead() )
+ scope( sb, xscope );
+
+ // Walk the Node edges
+ nodeEdges(sb, midParse && !xScopes.empty() ? xScopes.peek() : null, all);
+
+ // Walk the active Scope edges
+ if( midParse )
+ for( ScopeNode xscope : xScopes )
+ if( !xscope.isDead() )
+ scopeEdges( sb, xscope );
+
+
+ sb.p("}\n").di();
+ // Tell client another DOT frame
+ String dot = sb.toString();
+ System.out.println(dot);
+ try {
+ SERVER.put(dot);
+ } catch( IOException ioe ) {
+ try { SERVER.close();} catch( IOException ignored ){}
+ }
+ }
+
+
+ private static void nodes(SB sb, Collection<Node> all) {
+ // Just the Nodes first, in a cluster no edges
+ sb.i().p("subgraph cluster_Nodes {\n").ii(); // Magic "cluster_" in the subgraph name
+ for( Node n : all ) {
+ if( n instanceof ProjNode || n instanceof CProjNode || n instanceof MemMergeNode )
+ continue; // Do not emit, rolled into MultiNode or Scope cluster already
+ sb.i().p(n.uniqueName()).p(" [ ");
+ if( n instanceof MultiNode && !(n instanceof StartNode) ) {
+ // Make a box with the MultiNode on top, and all the projections on the bottom
+ sb. p("shape=plaintext label=<\n").ii();
+ sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"4\">\n");
+ // Row over single cell for node label
+ cell(sb.i().p("<TR>"), n.glabel(), n, null).p("</TR>\n");
+ // Row over single cell, for nested table
+ sb.i().p("<TR><TD>\n").ii();
+ sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">").p("\n");
+ sb.i().p("<TR>");
+ n._outputs.sort((x,y) -> x instanceof ProjNode xp && y instanceof ProjNode yp ? (xp._idx - yp._idx) : ((x==null ? 99999 : x._nid) - (y==null ? 99999 : y._nid)));
+ boolean empty_row=true;
+ for( Node use : n._outputs )
+ if( use instanceof MultiUse muse ) {
+ cell(sb,use.glabel(),use,"p"+muse.idx());
+ empty_row=false;
+ }
+ // At least one cell on row
+ if( empty_row ) sb.p("<TD></TD>");
+ sb. p("</TR>").p("\n");
+ sb.i().p("</TABLE>").p("\n").di();
+ sb.i().p("</TD></TR>\n");
+ sb.i().p("</TABLE>>\n").di();
+ sb.i().p("];\n");
+
+ } else {
+ // control nodes have box shape
+ // other nodes are ellipses, i.e. default shape
+ //else if (n instanceof PhiNode) sb.p("style=filled fillcolor=lightyellow ");
+ node(sb,n.glabel(),n);
+ sb.p("];\n");
+ }
+ }
+ // Force Region & Phis to line up
+ for (Node n : all) {
+ if (n instanceof RegionNode region) {
+ sb.i().p("{ rank=same; ");
+ sb.p(region.uniqueName()).p(";");
+ for (Node phi : region._outputs)
+ if (phi instanceof PhiNode) sb.p(phi.uniqueName()).p(";");
+ sb.p("}\n");
+ }
+ }
+ sb.di().i().p("}").nl(); // End Node cluster
+ }
+
+ // Build a nested scope display, walking the _prev edge
+ private static void scope( SB sb, ScopeNode scope ) {
+ sb.i().p("node [shape=plaintext];\n");
+ int last = scope.nIns();
+ int max = scope._lexSize.size()+1; // One more than lexsize
+ for( int i = 0; i < max; i++ ) {
+ int level = max-i-1;
+ String scopeName = makeScopeName(scope, level);
+ sb.i().p("subgraph cluster_").p(scopeName).p(" {\n").ii(); // Magic "cluster_" in the subgraph name
+ sb.i().p(scopeName).p(" [label=<\n").ii();
+ sb.i().p("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">\n");
+ int lexStart = level==0 ? 0 : scope._lexSize.at(level-1);
+
+ // Special for memory ScopeMinNode
+ MemMergeNode n = scope.nIns()>1 ? scope.mem() : null;
+ if( level==0 && n!=null && n.nIns()>2 ) {
+ sb.i().p("<TR>");
+ for( int m=2; m<n.nIns(); m++ )
+ cell(sb,"#"+m,n.alias(m),"m"+m);
+ sb.p("</TR>\n"); // End scope level
+ }
+
+ // Scope variables, empty if none
+ if( lexStart<last ) {
+ sb.i().p("<TR>\n");
+ for( int j=lexStart; j<last; j++ ) {
+ if( scope.in(j)==null ) continue;
+ var v = scope._vars.at(j);
+ cell(sb.i(),v._name,scope.in(j),makePortName(scopeName, v._name)).nl();
+ }
+ last = lexStart;
+ sb.i().p("</TR>\n");
+ }
+ sb.i().p("</TABLE>>];\n").di();
+ // Scope label
+ sb.i().p(scopeName).p("A [label=\"").p(level).p("\" shape=doublecircle style=filled fillcolor=aqua fontsize=10 margin=0 width=0.2 ];\n");
+ }
+ // Scope clusters nest, so the graphics shows the nested scopes, so
+ // they are not closed as they are printed; so they just keep nesting.
+ // We close them all at once here.
+ for( int i=0; i<max; i++ )
+ sb.di().i().p("}\n");
+ }
+
+ // Append a cell, with color
+ private static SB cell(SB sb, String text, Node n, String port) {
+ sb.p("<TD");
+ String color = color(n);
+ sb.p(" BGCOLOR=\"").p(color).p("\"");
+ if( port!=null ) sb.p(" PORT=\"").p(port).p("\"");
+ return colorcell(sb.p(">"),text,n,dark(color)).p("</TD>");
+ }
+ private static SB node(SB sb, String text, Node n) {
+ String color = color(n);
+ sb.p("style=filled fillcolor=").p(color).p(" ");
+ String shape = switch( n ) {
+ case FunNode fun -> "shape=box peripheries=2";
+ case CFGNode cfg -> "shape=box";
+ default -> "";
+ };
+ sb.p(shape).p(" ");
+ return colorcell(sb.p("label=<"),text,n,dark(color)).p(">");
+ }
+
+ private static String color(Node n) {
+ return switch( n ) {
+ case CFGNode cfg -> "yellow";
+ case NewNode nnn -> "lightgreen";
+ case StructNode s -> "lightgreen";
+ default -> typeColor(n._type);
+ };
+ }
+ private static String typeColor(Type t) {
+ return t==null ? "white" : switch( t ) {
+ case TypeMem mem -> "blue";
+ case TypeMemPtr tmp -> "green";
+ case TypeFunPtr tfp -> "orange";
+ case TypeRPC rpc -> "antiquewhite";
+ case TypeInteger ti -> "lightblue";
+ case TypeFloat tf -> "aqua";
+ case Type tn -> tn==Type.NIL ? "lightgray" : "red";
+ };
+ }
+ private static final String[] DARKS = new String[]{"blue","green","black"};
+ private static boolean dark(String color) { return Utils.find(DARKS,color) != -1; }
+
+
+
+ // Called with an open div
+ private static SB colorcell(SB sb, String text, Node n, boolean dark) {
+ if( dark ) sb.p("<font color=\"white\">");
+ sb.p(text);
+ if( n._type!=null ) {
+ sb.p("<br /><font point-size=\"10\">");
+ n._type.gprint(sb);
+ sb.p("</font>");
+ }
+ if( dark ) sb.p("</font>");
+ return sb;
+ }
+
+
+ private static String makeScopeName(ScopeNode sn, int level) { return sn.uniqueName() + "_" + level; }
+ private static String makePortName(String scopeName, String varName) { return scopeName + "_" + varName; }
+
+ // Walk the node edges
+ private static void nodeEdges(SB sb, ScopeNode scope, Collection<Node> all) {
+ // All them edge labels
+ String scopeName = scope!=null ? makeScopeName(scope, 0) : null;
+ sb.i().p("edge [ fontname=Helvetica, fontsize=8 ];\n");
+ for( Node n : all ) {
+ // Do not display the Constant->Start edge;
+ if( n instanceof ConstantNode ||
+ n instanceof XCtrlNode ||
+ // ProjNodes handled by Multi;
+ n instanceof ProjNode ||
+ n instanceof CProjNode ||
+ // ScopeNodes are done separately
+ n instanceof ScopeNode ||
+ n instanceof MemMergeNode
+ )
+ continue;
+ if( n.isDead() )
+ throw Utils.TODO();
+ // Normal edges
+ for( int i=0; i<n.nIns(); i++ ) {
+ Node def = n.in(i);
+ if( def==null ) continue;
+ sb.i().p(n.uniqueName()).p(" -> ");
+ if( n instanceof PhiNode && def instanceof RegionNode ) {
+ // Draw a dotted use->def edge from Phi to Region.
+ sb.p(def.uniqueName());
+ sb.p(" [style=dotted taillabel=").p(i).p("];\n");
+ } else {
+ // Most edges land here use->def
+ if( def instanceof CProjNode proj ) {
+ String mname = proj.ctrl().uniqueName();
+ sb.p(mname).p(":p").p(proj._idx);
+ } else if( def instanceof ProjNode proj ) {
+ String mname = proj.in(0).uniqueName();
+ sb.p(mname).p(":p").p(proj._idx);
+ } else sb.p(def.uniqueName());
+ // Number edges, so we can see how they track
+ sb.p("[taillabel=").p(i);
+ // Color the edge
+ nodeEdgeColor(sb,def._type);
+ // Backedges do not add a ranking constraint
+ if( i==2 && (n instanceof PhiNode || n instanceof LoopNode) )
+ sb.p(" constraint=false");
+ sb.p("];\n");
+ }
+ }
+
+ // Bonus edge if hooked by parser
+ if( (n.iskeep() || n.isUnused()) && scopeName != null ) {
+ sb.i().p(scopeName).p(" -> ").p(n.uniqueName()).p(" [ style=dashed color=grey];\n");
+ }
+ // Force functions above return
+ if( n instanceof ReturnNode ret && ret.inProgress() && !ret.fun().isDead() )
+ sb.i().p(ret.ctrl().uniqueName()).p(" -> ").p(ret.fun().uniqueName()).p("[style=invis]\n");
+ }
+ }
+
+ private static void nodeEdgeColor(SB sb, Type t) {
+ sb.p(" color=").p(typeColor(t));
+ }
+
+ // Walk the scope edges
+ private static void scopeEdges( SB sb, ScopeNode scope ) {
+ sb.i().p("edge [style=dashed color=cornflowerblue];\n");
+ int level=0;
+ for( var v : scope._vars ) {
+ if( v._idx==1 ) continue; // Memory done separately
+ Node def = scope.in(v._idx);
+ while( def instanceof ScopeNode lazy )
+ def = lazy.in(v._idx);
+ if( def==null ) continue;
+ while( level < scope._lexSize.size() && v._idx >= scope._lexSize.at(level) )
+ level++;
+ String scopeName = makeScopeName(scope, level);
+ sb.i()
+ .p(scopeName).p(":")
+ .p('"').p(makePortName(scopeName, v._name)).p('"') // wrap port name with quotes because $ctrl is not valid unquoted
+ .p(" -> ");
+ defPort(sb,def).p(";\n");
+ }
+
+ // Memory
+ if( scope.nIns()>1 ) {
+ MemMergeNode n = scope.mem();
+ for( int i=2; i<n.nIns(); i++ ) {
+ Node def = n.in(i);
+ while( def instanceof ScopeNode lazy )
+ def = lazy.in(i);
+ if( def==null ) continue;
+ String scopeName = makeScopeName(scope, 0);
+ sb.i().p(scopeName).p(":m").p(i).p(" -> ");
+ defPort(sb,def).p(";\n");
+ }
+ }
+
+ }
+
+ private static SB defPort(SB sb, Node def) {
+ return def instanceof MultiUse muse
+ ? sb.p(def.in(0).uniqueName()).p(":p").p(muse.idx())
+ : sb.p(def.uniqueName());
+ }
+}