summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-09-22 14:59:11 -0700
committerCliff Click <cliffc@acm.org>2026-09-22 14:59:11 -0700
commited13d1ca215ea094b4d6beab89b90ab4c05ed396 (patch)
treed273e5508c01566c51b03b322f750f7aeea6e3a5
parent29fb671e69f90ad3b04c6fa8d746caf857f89e77 (diff)
downloadSimple-ed13d1ca215ea094b4d6beab89b90ab4c05ed396.tar.gz
Simple-ed13d1ca215ea094b4d6beab89b90ab4c05ed396.zip
Tidy up scope display
-rw-r--r--chapter03/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter04/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter05/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter06/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter07/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter08/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter09/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter11/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter12/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter13/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter14/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter15/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter16/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter17/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter18/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter19/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter20/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter21/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter22/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter23/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter24/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java2
-rw-r--r--graph/README.md30
-rw-r--r--graph/src/main/java/com/seaofnodes/graph/GraphAdapter.java7
-rw-r--r--graph/src/main/java/com/seaofnodes/graph/GraphCapture.java18
-rw-r--r--graph/src/main/java/com/seaofnodes/graph/GraphJson.java2
-rw-r--r--graph/src/main/java/com/seaofnodes/graph/GraphSnapshot.java4
-rw-r--r--graph/web/index.html5
-rw-r--r--graph/web/layout.js161
-rw-r--r--graph/web/render.js64
-rw-r--r--graph/web/viewer.js12
32 files changed, 274 insertions, 75 deletions
diff --git a/chapter03/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter03/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 0e24e15d..2dc5d844 100644
--- a/chapter03/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter03/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -30,9 +30,9 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
- roots.add(_parser._scope);
roots.add(_ret);
}
}
diff --git a/chapter04/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter04/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index f2ca7888..d655ed4a 100644
--- a/chapter04/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter04/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -28,9 +28,9 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
- roots.add(_parser._scope);
roots.add(_ret);
}
}
diff --git a/chapter05/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter05/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index e88a7ffe..a7604165 100644
--- a/chapter05/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter05/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -30,10 +30,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter06/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter06/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 122c7267..ea9a705e 100644
--- a/chapter06/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter06/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -30,10 +30,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter07/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter07/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 1f4987d9..1835f53f 100644
--- a/chapter07/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter07/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -30,10 +30,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter08/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter08/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 7861b040..0c3f4ce8 100644
--- a/chapter08/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter08/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -30,10 +30,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter09/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter09/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 2f0b286c..2c6c90ca 100644
--- a/chapter09/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter09/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -33,10 +33,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 2b59f7ba..3f4eeeba 100644
--- a/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -33,10 +33,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter11/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter11/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index af20e147..622d927f 100644
--- a/chapter11/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter11/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -33,10 +33,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter12/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter12/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 3d9ec14a..10ac4fdb 100644
--- a/chapter12/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter12/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -33,10 +33,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter13/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter13/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index ee333ad7..c745dfaf 100644
--- a/chapter13/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter13/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -33,10 +33,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter14/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter14/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 5db8c01e..db9d4845 100644
--- a/chapter14/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter14/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -33,10 +33,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter15/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter15/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 57ea2dba..d3228b67 100644
--- a/chapter15/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter15/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -33,10 +33,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter16/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter16/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 8ca8f7ad..989a751b 100644
--- a/chapter16/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter16/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -33,10 +33,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter17/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter17/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index ab1d8c34..38f84d7d 100644
--- a/chapter17/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter17/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -33,10 +33,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
_parser = null;
_ret = null;
}
+ @Override protected Node scope() { return _parser._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(Parser.START);
roots.add(_parser.STOP);
- roots.add(_parser._scope);
roots.addAll(_parser._xScopes);
roots.add(_ret);
}
diff --git a/chapter18/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter18/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 975b289a..038a7e23 100644
--- a/chapter18/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter18/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -25,11 +25,11 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
if( _code != null ) _code._obs = null;
_code = null;
}
+ @Override protected Node scope() { return _code.P == null ? null : _code.P._scope; }
@Override protected void roots(ArrayList<Node> roots) {
roots.add(_code._stop);
if( _code.P != null ) {
roots.add(com.seaofnodes.simple.Parser.START);
- roots.add(_code.P._scope);
roots.addAll(_code.P._xScopes);
}
}
diff --git a/chapter19/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter19/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index b7f407fa..04c9298f 100644
--- a/chapter19/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter19/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -25,10 +25,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
if( _code != null ) _code._obs = null;
_code = null;
}
+ @Override protected Node scope() { return _code.P._scope; }
@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);
}
}
diff --git a/chapter20/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter20/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 5c284268..0a8f5f8d 100644
--- a/chapter20/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter20/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -25,10 +25,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
if( _code != null ) _code._obs = null;
_code = null;
}
+ @Override protected Node scope() { return _code.P._scope; }
@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);
}
}
diff --git a/chapter21/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter21/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index b04c58a2..d644663e 100644
--- a/chapter21/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter21/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -25,10 +25,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
if( _code != null ) _code._obs = null;
_code = null;
}
+ @Override protected Node scope() { return _code.P._scope; }
@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);
}
}
diff --git a/chapter22/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter22/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 584a16fe..68375627 100644
--- a/chapter22/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter22/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -25,10 +25,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
if( _code != null ) _code._obs = null;
_code = null;
}
+ @Override protected Node scope() { return _code.P._scope; }
@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);
}
}
diff --git a/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index 6e421c2c..44e76493 100644
--- a/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -25,10 +25,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
if( _code != null ) _code._obs = null;
_code = null;
}
+ @Override protected Node scope() { return _code.P._scope; }
@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);
}
}
diff --git a/chapter24/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java b/chapter24/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
index ee540508..b2704c50 100644
--- a/chapter24/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter24/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -25,10 +25,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
if( _code != null ) _code._obs = null;
_code = null;
}
+ @Override protected Node scope() { return _code.P._scope; }
@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);
}
}
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 7af718ac..0e5281fb 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java
@@ -25,10 +25,10 @@ public class SimpleGraphObserver extends GraphCapture<Node> {
if( _code != null ) _code._obs = null;
_code = null;
}
+ @Override protected Node scope() { return _code.P._scope; }
@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);
}
}
diff --git a/graph/README.md b/graph/README.md
index 21ee0d07..a2145220 100644
--- a/graph/README.md
+++ b/graph/README.md
@@ -105,7 +105,7 @@ The JSON frame has this shape:
```json
{
"snap": {
- "ver": 1, "comp": "compilation UUID", "step": 0, "roots": [1],
+ "ver": 1, "comp": "compilation UUID", "step": 0, "roots": [1], "scope": 0,
"nodes": [
{"id": 1, "label": "Start", "type": null, "kind": "CTRL", "edges": [], "proj": null}
]
@@ -124,6 +124,9 @@ program, and `step` starts at zero. `nodes` carries the complete graph, includin
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.
+`scope` identifies the parser's active scope (zero outside parsing); scope nodes
+in `roots` include the parser's saved scopes. These are context references,
+not IR edges.
The browser caches the parsed objects in `frames`. It computes geometry on the
first visit to a frame and keeps it in `frames[i].layout`; revisiting a frame
@@ -137,8 +140,27 @@ uses ELK's layered layout with fixed input ports and orthogonal routing.
Layout follows def-to-use flow downward, while the displayed arrows point
from use to def, matching Simple's edges. Control edges get a higher layout
priority; known Loop/Phi backedges get a lower priority. Associations are drawn
-as a separate overlay and do not constrain layout. Projections remain real
-nodes for now.
+as a separate overlay and do not constrain layout. Each MultiNode and its
+projections occupy one box: the parent above, projection cells below in index
+order. Each cell keeps its node ID, selection, highlights and edge connections;
+the internal parent/projection edge is represented by the shared box. A
+projection whose parent is absent is drawn on its own. Node kinds use color
+and shape rather than a KIND label.
+
+Scopes have named slots across the top, including control, memory and variable
+bindings. Edges leave these slots upward toward their definitions. They stay visible
+even when **Associations** is unchecked, and do not constrain CFG layout.
+While parsing, saved scopes line up beside the graph, slightly below their
+`$ctrl` definitions, with space to avoid overlapping other scopes. The active
+scope stays at the bottom. The **Parser** box points to both active and saved
+scopes using display-only arrows.
+
+During parsing, values with no graph users yet appear in a separate bottom
+row, with dashed display-only arrows from a **Parser**
+box. Scope bindings count as users. Pending projections keep their parent box
+and get an arrow to their own cell. These marks disappear when real uses attach
+or parsing finishes; they do not add nodes or edges to the compiler graph.
+This identifies unconsumed values from the snapshot, not Java stack references.
This first layout is flat. It does not discover compiler loops, compute RPO,
or build a SESE hierarchy. ELK breaks cycles for drawing, which can still put
@@ -162,7 +184,7 @@ The shared Java code is in `src/main/java/com/seaofnodes/graph/`:
- `GraphAdapter<N>` is an abstract base with hooks for `id`, `desc`, and indexed
edge access (`nIns`/`in`, `nOuts`/`out`). Its final `snap` method walks definitions and
uses iteratively, handles cycles, and sorts the copied records by ID.
-- `GraphSnapshot` is detached data: compilation key, step, roots, and nodes.
+- `GraphSnapshot` is detached data: compilation key, step, roots, active scope, and nodes.
Each node has an ID, plain-text label/type, kind, edges to its defs, and optional
projection metadata. It contains no chapter classes or layout coordinates.
Node and edge lists are concrete `ArrayList`s, treated as read-only after capture.
diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphAdapter.java b/graph/src/main/java/com/seaofnodes/graph/GraphAdapter.java
index 3e840b54..47bf3444 100644
--- a/graph/src/main/java/com/seaofnodes/graph/GraphAdapter.java
+++ b/graph/src/main/java/com/seaofnodes/graph/GraphAdapter.java
@@ -37,6 +37,11 @@ public abstract class GraphAdapter<N> {
}
public final GraphSnapshot snap(String comp, long step, ArrayList<N> roots) {
+ return snap(comp, step, roots, 0);
+ }
+
+ /** scope names the active parser scope, which the caller includes in roots. */
+ public final GraphSnapshot snap(String comp, long step, ArrayList<N> roots, int scope) {
// Node IDs are dense; index directly without boxed keys or map entries.
var seen = new ArrayList<N>();
var todo = new ArrayDeque<N>();
@@ -55,7 +60,7 @@ public abstract class GraphAdapter<N> {
// Determinism without reordering the compiler's use lists.
nodes.sort(Comparator.comparingInt(GraphSnapshot.Node::id));
return new GraphSnapshot(GraphSnapshot.VER, comp, step,
- Arrays.copyOf(rids, len), nodes);
+ Arrays.copyOf(rids, len), scope, nodes);
}
private boolean enq(N node, ArrayList<N> seen, ArrayDeque<N> todo) {
diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphCapture.java b/graph/src/main/java/com/seaofnodes/graph/GraphCapture.java
index 6fc40b5f..4be65392 100644
--- a/graph/src/main/java/com/seaofnodes/graph/GraphCapture.java
+++ b/graph/src/main/java/com/seaofnodes/graph/GraphCapture.java
@@ -23,6 +23,8 @@ public abstract class GraphCapture<N> extends GraphObserver<N> {
protected abstract String phase();
protected abstract int pos();
protected abstract void roots(ArrayList<N> roots);
+ // Called only during parsing; the shared capture adds this root too.
+ protected N scope() { return null; }
protected abstract void detach();
final void run(String src, GraphSocket server) {
@@ -65,7 +67,7 @@ public abstract class GraphCapture<N> extends GraphObserver<N> {
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);
+ p.pre = snap(n, true);
}
@Override public void after(N n, N repl, boolean applied) {
@@ -85,7 +87,7 @@ public abstract class GraphCapture<N> extends GraphObserver<N> {
up.sent = true;
}
N rez = repl == null ? n : repl;
- emit(snap(_graph.dead(rez) ? null : rez), pos(),
+ emit(snap(_graph.dead(rez) ? null : rez, true), pos(),
evt(p, applied ? GraphEvent.Kind.APPLY : GraphEvent.Kind.RETURN,
_graph.dead(rez) ? 0 : _graph.id(rez)));
}
@@ -101,7 +103,7 @@ public abstract class GraphCapture<N> extends GraphObserver<N> {
@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]));
+ emit(snap(null, false), pos(), new GraphEvent(GraphEvent.Kind.PHASE, 0, 0, phase, 0, 0, new int[0]));
}
private GraphEvent evt(Peep p, GraphEvent.Kind kind, int repl) {
@@ -121,18 +123,22 @@ public abstract class GraphCapture<N> extends GraphObserver<N> {
}
- private GraphSnapshot snap(N extra) {
+ private GraphSnapshot snap(N extra, boolean parsing) {
var roots = new ArrayList<N>();
roots(roots);
+ // Early parsers retain their emptied scope after returning. A completed
+ // phase no longer owns that scope, even while phase() still says Parse.
+ N scope = parsing && "Parse".equals(phase()) ? scope() : null;
+ roots.add(scope);
for( Peep p : _peeps ) if( !_graph.dead(p.node) ) roots.add(p.node);
roots.add(extra);
- return _graph.snap(_comp, 0, roots);
+ return _graph.snap(_comp, 0, roots, _graph.ref(scope));
}
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());
+ snap = new GraphSnapshot(snap.ver(), _comp, _step++, snap.roots(), snap.scope(), snap.nodes());
try {
_server.put(GraphJson.frame(snap, pos, evt));
} catch( IOException e ) {
diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphJson.java b/graph/src/main/java/com/seaofnodes/graph/GraphJson.java
index 8726d81b..c920763a 100644
--- a/graph/src/main/java/com/seaofnodes/graph/GraphJson.java
+++ b/graph/src/main/java/com/seaofnodes/graph/GraphJson.java
@@ -42,7 +42,7 @@ public class GraphJson {
if( i > 0 ) sb.append(',');
sb.append(roots[i]);
}
- sb.append("],\"nodes\":[");
+ sb.append("],\"scope\":").append(snap.scope()).append(",\"nodes\":[");
var nodes = snap.nodes();
for( int i = 0; i < nodes.size(); i++ ) {
if( i > 0 ) sb.append(',');
diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphSnapshot.java b/graph/src/main/java/com/seaofnodes/graph/GraphSnapshot.java
index 18926726..6836693e 100644
--- a/graph/src/main/java/com/seaofnodes/graph/GraphSnapshot.java
+++ b/graph/src/main/java/com/seaofnodes/graph/GraphSnapshot.java
@@ -2,9 +2,9 @@ package com.seaofnodes.graph;
import java.util.ArrayList;
-/** A detached graph. Treat its lists as read-only after capture. */
+/** A detached graph. Lists are read-only; scope is the active parser scope ID, or zero. */
public record GraphSnapshot(int ver, String comp, long step,
- int[] roots, ArrayList<Node> nodes) {
+ int[] roots, int scope, ArrayList<Node> nodes) {
public static final int VER = 1;
public GraphSnapshot {
diff --git a/graph/web/index.html b/graph/web/index.html
index 771d3d4a..a568cbc6 100644
--- a/graph/web/index.html
+++ b/graph/web/index.html
@@ -19,9 +19,12 @@
.edge { fill:none; stroke-width:1.5; }
.edge.CTRL { stroke-width:2; }
.edge.ASSOC { stroke-dasharray:4 4; }
+ .edge.PARSER { stroke-dasharray:5 4; }
+ .parser rect { fill:#f6f0ff; stroke:#7956a5; stroke-width:1.5; stroke-dasharray:5 4; }
.edge.selected { stroke-width:3.5; }
.node { cursor:pointer; }
.box { stroke:#758191; stroke-width:1; }
+ .node.held .box { stroke-dasharray:5 3; }
.node.near .box { stroke:#c18221; stroke-width:2; }
.node.focus .box { stroke:#9c4900; stroke-width:3; }
.node.selected .box { stroke:#1569b9; stroke-width:3; }
@@ -29,6 +32,8 @@
.label { font-size:12px; fill:#152330; font-weight:600; }
.type { font-size:11px; fill:#455768; }
.port { stroke:#536477; stroke-width:1; }
+ .row { stroke:#b6afcf; stroke-width:1; }
+ .bind { font-size:12px; fill:#152330; }
.slot { font-size:10px; fill:#364352; paint-order:stroke; stroke:#fafbfc; stroke-width:3; }
#detail { position:absolute; top:8px; right:8px; margin:0; padding:12px; max-width:320px;
max-height:45%; overflow:auto; white-space:pre-wrap; overflow-wrap:anywhere;
diff --git a/graph/web/layout.js b/graph/web/layout.js
index 1cf9fb14..a277cb53 100644
--- a/graph/web/layout.js
+++ b/graph/web/layout.js
@@ -11,31 +11,94 @@ class GraphLayout {
return chars.length > 44 ? chars.slice(0, 43).join("") + "…" : chars.join("");
}
- async run(snap) {
+ async run(snap, evt) {
const nodes = snap.nodes.map(n => {
const label = this.clip(n.label), type = this.clip(n.type);
- const w = Math.ceil(Math.max(120, this.ctx.measureText(label).width + 24,
+ let w = Math.ceil(Math.max(120, this.ctx.measureText(label).width + 24,
this.ctx.measureText(type).width + 24, (n.edges.length + 1) * 18));
- const h = 62;
- const ports = n.edges.map(e => ({
- id: "n" + n.id + "i" + e.idx, x: w * (e.idx + 1) / (n.edges.length + 1) - 3,
- y: -3, width: 6, height: 6, layoutOptions: {"elk.port.side": "NORTH"}
- }));
- ports.push({id: "n" + n.id + "o", x: w / 2 - 3, y: h - 3,
- width: 6, height: 6, layoutOptions: {"elk.port.side": "SOUTH"}});
- return {id: "n" + n.id, width: w, height: h, ports,
- layoutOptions: {"elk.portConstraints": "FIXED_POS"}, n, label, type};
+ const scope = n.kind === "SCOPE";
+ const cols = scope ? n.edges.map(e => Math.ceil(Math.max(48,
+ this.ctx.measureText(e.label || "[" + e.idx + "]").width + 24))) : [];
+ if (scope) w = Math.ceil(Math.max(120, this.ctx.measureText(label).width + 24,
+ cols.reduce((sum, w) => sum + w, 0)));
+ return {id: "n" + n.id, width: w, height: 62,
+ ports: [], n, label, type, scope, cols, ox: 0, oy: 0};
});
const byId = new Map(nodes.map(n => [n.n.id, n]));
+ const parsing = evt?.phase === "Parse" && evt.kind !== "PHASE";
+ const roots = new Set(snap.roots);
+ const scopes = nodes.filter(n => n.scope);
+ const active = parsing ? byId.get(snap.scope) : null;
+ // Scope bindings count as users too. Scope ownership comes from parser
+ // roots; unfinished expression values are inferred from absent uses.
+ const used = new Set();
+ for (const n of snap.nodes) for (const e of n.edges) if (e.def) used.add(e.def);
+ const held = parsing ? nodes.filter(n =>
+ (n.n.proj || ["DATA", "MEM", "PHI"].includes(n.n.kind)) && !used.has(n.n.id)) : [];
+ for (const n of held) n.held = true;
+ const owned = [...held, ...scopes.filter(n => parsing && roots.has(n.n.id))];
+ const projs = new Map();
+ for (const n of nodes) {
+ const par = byId.get(n.n.proj?.par);
+ // A projection without its parent still gets a standalone box.
+ if (!par || par.n.proj) continue;
+ if (!projs.has(par)) projs.set(par, []);
+ projs.get(par).push(n);
+ n.par = par;
+ }
+ const groups = nodes.filter(n => !n.par).map(n => {
+ const slots = (projs.get(n) || []).sort((a, b) => a.n.proj.idx - b.n.proj.idx || a.n.id - b.n.id);
+ const parts = [n, ...slots];
+ if (slots.length) {
+ const w = slots.reduce((sum, p) => sum + p.width, 0);
+ n.width = Math.max(n.width, w);
+ const extra = (n.width - w) / slots.length;
+ let x = 0;
+ for (const p of slots) {
+ p.width += extra;
+ p.ox = x;
+ p.oy = n.height;
+ x += p.width;
+ }
+ }
+ const ports = [];
+ for (const p of parts) {
+ p.cell = slots.length > 0;
+ const extra = p.scope && p.cols.length ?
+ (p.width - p.cols.reduce((sum, w) => sum + w, 0)) / p.cols.length : 0;
+ let x = 0;
+ for (const [idx, e] of p.n.edges.entries()) {
+ // The parent/projection link is represented by the shared box.
+ if (p.par && e.idx === 0 && e.def === p.par.n.id) continue;
+ if (p.scope) {
+ const span = p.cols[idx] + extra;
+ ports.push({id: p.id + "i" + e.idx, x: x + span / 2 - 3, y: -3, span,
+ width: 6, height: 6, layoutOptions: {"elk.port.side": "NORTH"}});
+ x += span;
+ continue;
+ }
+ ports.push({id: p.id + "i" + e.idx, x: p.ox + p.width * (e.idx + 1) / (p.n.edges.length + 1) - 3,
+ y: p.oy - 3, width: 6, height: 6, layoutOptions: {"elk.port.side": "NORTH"}});
+ }
+ // Direct uses of the MultiNode attach to its header, projections below.
+ const side = p === n && slots.length ? "EAST" : "SOUTH";
+ ports.push({id: p.id + "o", x: p.ox + (side === "EAST" ? p.width : p.width / 2) - 3,
+ y: p.oy + (side === "EAST" ? p.height / 2 : p.height) - 3,
+ width: 6, height: 6, layoutOptions: {"elk.port.side": side}});
+ }
+ return {id: n.id, width: n.width, height: n.height * (slots.length ? 2 : 1), ports, parts};
+ });
const edges = [];
for (const use of snap.nodes) {
for (const e of use.edges) {
if (!e.def) continue;
+ if (byId.get(use.id).par && e.idx === 0 && e.def === use.proj.par) continue;
const id = "e" + use.id + "i" + e.idx;
const reg = byId.get(use.edges[0]?.def)?.n;
const back = e.idx === 2 && (use.kind === "LOOP" ||
(use.kind === "PHI" && reg?.kind === "LOOP"));
edges.push({id, use: use.id, def: e.def, idx: e.idx, role: e.role, label: e.label,
+ bind: use.kind === "SCOPE",
// Layout follows value/control flow downward. SVG arrows point back
// from use to def, matching Simple's actual edge direction.
sources: ["n" + e.def + "o"], targets: ["n" + use.id + "i" + e.idx],
@@ -43,31 +106,83 @@ class GraphLayout {
}
}
const graph = await this.elk.layout({
- id: "root", children: nodes.map(({n, label, type, ...box}) => box),
+ id: "root", children: groups.filter(g => !g.parts[0].scope).map(({parts, ...box}) => ({...box,
+ layoutOptions: {"elk.portConstraints": "FIXED_POS",
+ // Keep a projection with its parent even when that parent has real uses.
+ "elk.layered.layering.layerConstraint": parts.length === 1 && parts[0].held ? "LAST_SEPARATE" : "NONE"}
+ })),
// Scope bindings and lifetime associations must not impose CFG ranks.
- edges: edges.filter(e => e.role !== "ASSOC").map(e => ({
+ edges: edges.filter(e => e.role !== "ASSOC" && !e.bind && !byId.get(e.def).scope).map(e => ({
id: e.id, sources: e.sources, targets: e.targets, layoutOptions: e.layoutOptions
})),
layoutOptions: {
"elk.algorithm": "layered", "elk.direction": "DOWN", "elk.edgeRouting": "ORTHOGONAL",
"elk.spacing.nodeNode": 32, "elk.layered.spacing.nodeNodeBetweenLayers": 48,
"elk.padding": "[top=24,left=24,bottom=24,right=24]",
+ "elk.separateConnectedComponents": held.length === 0,
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
"elk.randomSeed": 1
}
});
+ // Scope ports use the same local geometry; place their boxes after CFG nodes.
+ for (const g of groups.filter(g => g.parts[0].scope)) graph.children.push({...g, x: 0, y: 0});
const boxes = new Map(graph.children.map(n => [n.id, n]));
- const routes = new Map(graph.edges.map(e => [e.id, e]));
- for (const n of nodes) {
- const box = boxes.get(n.id);
- n.x = box.x; n.y = box.y;
- // Keep ports in input-slot order even if ELK reorders its result.
+ const routes = new Map((graph.edges || []).map(e => [e.id, e]));
+ for (const group of groups) {
+ const box = boxes.get(group.id);
const ports = new Map(box.ports.map(p => [p.id, p]));
- n.ports = n.ports.map(p => ports.get(p.id));
+ for (const n of group.parts) {
+ n.x = box.x + n.ox; n.y = box.y + n.oy;
+ // Keep semantic nodes/slots intact for selection and neighborhood marks.
+ n.ports = n.n.edges.map(e => {
+ const p = ports.get(n.id + "i" + e.idx);
+ return p && {...p, x: p.x - n.ox, y: p.y - n.oy, edge: e};
+ }).filter(Boolean);
+ const out = ports.get(n.id + "o");
+ n.ports.push({...out, x: out.x - n.ox, y: out.y - n.oy});
+ }
+ }
+ // Saved scopes describe suspended parser states at their control points.
+ // Keep a common column, nudging down only when two name tables overlap.
+ const ctrlY = n => {
+ const ctrl = byId.get(n.n.edges.find(e => e.label === "$ctrl" || e.idx === 0)?.def);
+ return ctrl ? ctrl.y + ctrl.height + 24 : 24;
+ };
+ const saved = scopes.filter(n => n !== active).sort((a, b) => ctrlY(a) - ctrlY(b) || a.n.id - b.n.id);
+ if (saved.length) {
+ const x = graph.width + 48;
+ let y = 24;
+ for (const n of saved) {
+ n.x = x;
+ n.y = Math.max(y, ctrlY(n));
+ y = n.y + n.height + 24;
+ }
+ graph.width = x + Math.max(...saved.map(n => n.width)) + 24;
+ graph.height = Math.max(graph.height, y);
+ }
+ // The active scope moves with the parser, below the current graph.
+ if (active) {
+ graph.width = Math.max(graph.width, active.width + 48);
+ active.x = (graph.width - active.width) / 2;
+ active.y = graph.height + 24;
+ graph.height = active.y + active.height + 24;
+ }
+ // Parser ownership is an overlay, never an IR node or an ELK flow edge.
+ const parser = owned.length ? {width: Math.max(180, (owned.length + 1) * 18), height: 36} : null;
+ if (parser) {
+ graph.width = Math.max(graph.width, parser.width + 48);
+ parser.x = (graph.width - parser.width) / 2;
+ parser.y = graph.height + 24;
+ graph.height = parser.y + parser.height + 24;
+ owned.sort((a, b) => a.x - b.x || a.n.id - b.n.id);
+ parser.ports = owned.map((n, i) => ({id: "parser-i" + n.n.id,
+ x: parser.width * (i + 1) / (owned.length + 1) - 3, y: -3}));
+ for (const n of owned) edges.push({id: "parser-hold-" + n.n.id, scope: n.scope, active: n === active,
+ use: 0, def: n.n.id, role: "PARSER", sources: [n.id + "o"], targets: ["parser-i" + n.n.id]});
}
for (const e of edges) {
- if (e.role === "ASSOC") {
- const def = byId.get(e.def), use = byId.get(e.use);
+ if (e.role === "ASSOC" || e.role === "PARSER" || e.bind || byId.get(e.def).scope) {
+ const def = byId.get(e.def), use = e.role === "PARSER" ? parser : byId.get(e.use);
const a = def.ports.find(p => p.id === e.sources[0]);
const b = use.ports.find(p => p.id === e.targets[0]);
const x = def.x + a.x + 3, y = def.y + a.y + 3;
@@ -79,6 +194,8 @@ class GraphLayout {
.map((p, i) => `${i ? "L" : "M"}${p.x},${p.y}`).join(" ")).join(" ");
}
}
- return {width: graph.width, height: graph.height, nodes, edges};
+ return {width: graph.width, height: graph.height, nodes, edges,
+ parser, scope: active?.n.id || 0, scopes: new Set(owned.filter(n => n.scope).map(n => n.n.id)),
+ held: new Set(held.map(n => n.n.id))};
}
}
diff --git a/graph/web/render.js b/graph/web/render.js
index 76cf8c06..34c66a02 100644
--- a/graph/web/render.js
+++ b/graph/web/render.js
@@ -6,7 +6,7 @@ class GraphView {
this.pick = 0;
this.auto = true;
this.scene = null;
- this.colors = {CTRL: "#b63838", DATA: "#536477", MEM: "#2867b2", ASSOC: "#9396a3"};
+ this.colors = {CTRL: "#b63838", DATA: "#536477", MEM: "#2867b2", ASSOC: "#9396a3", PARSER: "#7956a5"};
this.svg = d3.select(host).append("svg").attr("aria-label", "Compiler graph");
const defs = this.svg.append("defs");
for (const [role, color] of Object.entries(this.colors)) {
@@ -17,6 +17,7 @@ class GraphView {
}
this.world = this.svg.append("g");
this.links = this.world.append("g").attr("class", "edges");
+ this.parser = this.world.append("g").attr("class", "parser");
this.nodes = this.world.append("g").attr("class", "nodes");
this.zoom = d3.zoom().scaleExtent([.02, 4]).on("zoom", event => {
if (event.sourceEvent) this.auto = false;
@@ -46,11 +47,24 @@ class GraphView {
.attr("marker-start", e => "url(#arrow-" + e.role + ")")
.each(function(e) {
d3.select(this).selectAll("title").data([e]).join("title")
- .text(`#${e.use}[${e.idx}] → #${e.def} · ${e.role}${e.label ? " · " + e.label : ""}`);
+ .text(e.role === "PARSER" ? (e.scope ? `Parser holds ${e.active ? "active" : "saved"} scope #${e.def}` :
+ `Parser holds #${e.def}; no graph users yet`) :
+ `#${e.use}[${e.idx}] → #${e.def} · ${e.role}${e.label ? " · " + e.label : ""}`);
+ });
+ this.parser.selectAll("g").data(scene.parser ? [scene.parser] : []).join(enter => {
+ const g = enter.append("g");
+ g.append("rect").attr("rx", 6);
+ g.append("text").attr("class", "label").attr("text-anchor", "middle").attr("y", 22).text("Parser");
+ g.append("title").text("Display-only references to parser scopes and unfinished values; these are not compiler edges.");
+ return g;
+ }).attr("transform", p => `translate(${p.x},${p.y})`)
+ .each(function(p) {
+ d3.select(this).select("rect").attr("width", p.width).attr("height", p.height);
+ d3.select(this).selectAll("text").attr("x", p.width / 2);
});
const boxes = this.nodes.selectAll("g.node").data(scene.nodes, n => n.id).join(enter => {
const g = enter.append("g").attr("class", "node");
- g.append("rect").attr("class", "box");
+ g.append("path").attr("class", "box");
g.append("text").attr("class", "head").attr("x", 8).attr("y", 15);
g.append("text").attr("class", "label").attr("text-anchor", "middle").attr("y", 33);
g.append("text").attr("class", "type").attr("text-anchor", "middle").attr("y", 51);
@@ -58,25 +72,34 @@ class GraphView {
return g;
});
boxes.attr("id", n => n.id).attr("data-id", n => n.n.id)
+ .classed("held", n => !!n.held)
.attr("transform", n => `translate(${n.x},${n.y})`)
.on("click", (event, n) => { event.stopPropagation(); this.select(n.n.id); });
- boxes.select("rect.box").attr("width", n => n.width).attr("height", n => n.height)
- .attr("rx", n => ["CTRL", "REGION", "LOOP", "FUN", "UNIT"].includes(n.n.kind) ? 3 : 14)
+ boxes.select("path.box").attr("d", n => this.shape(n))
.attr("fill", n => ({CTRL: "#fff1c4", REGION: "#fff1c4", LOOP: "#ffe0ab", FUN: "#ffe0ab",
UNIT: "#ffe0ab", MEM: "#dcecff", PHI: "#f5e4fa", SCOPE: "#e5e0ff", DATA: "#edf3f7"})[n.n.kind]);
- boxes.select("text.head").text(n => `#${n.n.id} · ${n.n.kind}${n.n.proj ? " · p" + n.n.proj.idx : ""}`);
- boxes.select("text.label").attr("x", n => n.width / 2).text(n => n.label);
- boxes.select("text.type").attr("x", n => n.width / 2).text(n => n.type);
+ boxes.select("text.head").attr("y", n => n.scope ? 47 : 15)
+ .text(n => `#${n.n.id}${n.n.proj ? " · p" + n.n.proj.idx : ""}`);
+ boxes.select("text.label").attr("x", n => n.width / 2).attr("y", n => n.scope ? 51 : 33)
+ .text(n => n.label);
+ boxes.select("text.type").attr("x", n => n.width / 2).text(n => n.scope ? "" : n.type);
boxes.select("title").text(n => this.info(n.n));
boxes.each((n, i, groups) => {
const ports = n.ports.filter(p => p.id !== n.id + "o");
const group = d3.select(groups[i]);
+ group.selectAll("line.row").data(n.scope ? ports : [], p => p.id).join("line")
+ .attr("class", "row").attr("x1", (p, i) => i ? p.x + 3 - p.span / 2 : 0)
+ .attr("x2", (p, i) => i ? p.x + 3 - p.span / 2 : n.width)
+ .attr("y1", (p, i) => i ? 0 : 30).attr("y2", 30);
+ group.selectAll("text.bind").data(n.scope ? ports : [], p => p.id).join("text")
+ .attr("class", "bind").attr("x", p => p.x + 3).attr("y", 20).attr("text-anchor", "middle")
+ .text(p => p.edge.label || "[" + p.edge.idx + "]");
group.selectAll("circle.port").data(ports, p => p.id).join("circle")
.attr("class", "port").attr("cx", p => p.x + 3).attr("cy", p => p.y + 3)
- .attr("r", 3).attr("fill", (p, j) => n.n.edges[j].def ? "#536477" : "white");
+ .attr("r", 3).attr("fill", p => p.edge.def ? "#536477" : "white");
group.selectAll("text.slot").data(ports, p => p.id).join("text")
.attr("class", "slot").attr("x", p => p.x + 3).attr("y", p => p.y - 4)
- .attr("text-anchor", "middle").text((p, j) => n.n.edges[j].idx);
+ .attr("text-anchor", "middle").text(p => p.edge.idx);
});
this.assocs(document.getElementById("assocs").checked);
this.mark();
@@ -84,8 +107,23 @@ class GraphView {
if (this.auto) this.fit();
}
+ shape(n) {
+ const w = n.width, h = n.height;
+ // A MultiNode and its projection cells tile one rectangular box.
+ if (n.cell || n.n.kind === "CTRL" || n.n.kind === "UNIT") return `M0,0H${w}V${h}H0Z`;
+ if (n.n.kind === "REGION") return `M12,0H${w - 12}L${w},${h}H0Z`;
+ if (n.n.kind === "PHI") return `M12,0H${w - 12}L${w},${h / 2}L${w - 12},${h}H12L0,${h / 2}Z`;
+ if (n.n.kind === "MEM" || n.n.kind === "FUN")
+ return `M10,0H${w - 10}L${w},10V${h - 10}L${w - 10},${h}H10L0,${h - 10}V10Z`;
+ const r = n.n.kind === "LOOP" ? 24 : n.n.kind === "SCOPE" ? 4 : 14;
+ return `M${r},0H${w - r}Q${w},0 ${w},${r}V${h - r}Q${w},${h} ${w - r},${h}` +
+ `H${r}Q0,${h} 0,${h - r}V${r}Q0,0 ${r},0Z`;
+ }
+
info(n) {
- return `#${n.id} ${n.label}\n${n.kind}${n.proj ? " · projection " + n.proj.idx + " of #" + n.proj.par : ""}` +
+ return `#${n.id} ${n.label}${n.proj ? "\nProjection " + n.proj.idx + " of #" + n.proj.par : ""}` +
+ (this.scene.held.has(n.id) ? "\nHeld by parser; no graph users yet" : "") +
+ (this.scene.scopes.has(n.id) ? (this.scene.scope === n.id ? "\nActive parser scope" : "\nSaved parser scope") : "") +
`\n${n.type || "Type not computed"}\n\n` + n.edges.map(e =>
`[${e.idx}] → ${e.def ? "#" + e.def : "—"} ${e.role}${e.label ? " " + e.label : ""}`).join("\n");
}
@@ -94,13 +132,13 @@ class GraphView {
this.pick = id;
const node = this.scene?.nodes.find(n => n.n.id === id);
this.nodes.selectAll("g.node").classed("selected", n => n.n.id === id);
- this.links.selectAll("path.edge").classed("selected", e => e.use === id || e.def === id);
+ this.links.selectAll("path.edge").classed("selected", e => id !== 0 && (e.use === id || e.def === id));
const detail = document.getElementById("detail");
detail.hidden = !id;
detail.textContent = node ? this.info(node.n) : `#${id} is absent in this frame.`;
}
- assocs(show) { this.links.selectAll(".ASSOC").style("display", show ? null : "none"); }
+ assocs(show) { this.links.selectAll(".ASSOC").style("display", e => show || e.bind ? null : "none"); }
mark() {
const evt = this.evt;
diff --git a/graph/web/viewer.js b/graph/web/viewer.js
index c36c369c..63ee195e 100644
--- a/graph/web/viewer.js
+++ b/graph/web/viewer.js
@@ -81,11 +81,17 @@ async function render(index) {
const frame = frames[index];
busy = !frame.layout ? "Laying out frame " + (index + 1) + "..." : "Drawing...";
updateUI();
- if (frame.pos >= 0) program.setSelectionRange(frame.pos, frame.pos + 1);
try {
if (!layout) layout = new GraphLayout();
- if (!frame.layout) frame.layout = await layout.run(frame.snap);
- if (generation === frameGeneration) renderer.show(frame.snap, frame.layout, frame.evt);
+ if (!frame.layout) frame.layout = await layout.run(frame.snap, frame.evt);
+ if (generation === frameGeneration) {
+ renderer.show(frame.snap, frame.layout, frame.evt);
+ if (frame.pos >= 0) {
+ // Stepping focuses a button; focus the source so its selection is visible.
+ program.focus({preventScroll: true});
+ program.setSelectionRange(frame.pos, frame.pos + 1);
+ }
+ }
rendering = false;
busy = "";
if (generation === frameGeneration) current = index;