From a32e83c9d81cfdb3759a2961d8b192ca44d75935 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Fri, 25 Sep 2026 15:00:00 -0700 Subject: Bunch more animation work --- .../java/com/seaofnodes/simple/node/StopNode.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 + .../java/com/seaofnodes/simple/node/StopNode.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 + .../java/com/seaofnodes/simple/node/StopNode.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 + .../java/com/seaofnodes/simple/node/StopNode.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 + .../java/com/seaofnodes/simple/node/StopNode.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 + .../java/com/seaofnodes/simple/node/StopNode.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 + .../java/com/seaofnodes/simple/node/StopNode.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 + .../java/com/seaofnodes/simple/node/StopNode.java | 10 +- .../simple/print/SimpleGraphObserver.java | 2 + .../main/java/com/seaofnodes/simple/CodeGen.java | 7 +- .../com/seaofnodes/simple/node/CallEndNode.java | 1 + .../java/com/seaofnodes/simple/node/FunNode.java | 1 + .../simple/print/SimpleGraphAdapter.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 +- .../main/java/com/seaofnodes/simple/CodeGen.java | 7 +- .../com/seaofnodes/simple/node/CallEndNode.java | 1 + .../java/com/seaofnodes/simple/node/FunNode.java | 1 + .../simple/print/SimpleGraphAdapter.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 +- .../com/seaofnodes/simple/codegen/CodeGen.java | 7 +- .../com/seaofnodes/simple/node/CallEndNode.java | 1 + .../java/com/seaofnodes/simple/node/FunNode.java | 1 + .../simple/print/SimpleGraphAdapter.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 +- .../com/seaofnodes/simple/codegen/CodeGen.java | 7 +- .../com/seaofnodes/simple/node/CallEndNode.java | 1 + .../java/com/seaofnodes/simple/node/FunNode.java | 1 + .../simple/print/SimpleGraphAdapter.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 +- .../com/seaofnodes/simple/codegen/CodeGen.java | 7 +- .../com/seaofnodes/simple/node/CallEndNode.java | 1 + .../java/com/seaofnodes/simple/node/FunNode.java | 1 + .../simple/print/SimpleGraphAdapter.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 +- .../com/seaofnodes/simple/codegen/CodeGen.java | 7 +- .../com/seaofnodes/simple/node/CallEndNode.java | 1 + .../java/com/seaofnodes/simple/node/FunNode.java | 1 + .../simple/print/SimpleGraphAdapter.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 +- .../com/seaofnodes/simple/codegen/CodeGen.java | 7 +- .../com/seaofnodes/simple/node/CallEndNode.java | 1 + .../java/com/seaofnodes/simple/node/FunNode.java | 1 + .../simple/print/SimpleGraphAdapter.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 +- chapter25/pom.xml | 4 +- .../com/seaofnodes/simple/codegen/CodeGen.java | 9 +- .../com/seaofnodes/simple/node/CallEndNode.java | 2 +- .../java/com/seaofnodes/simple/node/FunNode.java | 1 + .../simple/print/SimpleGraphAdapter.java | 12 +- .../simple/print/SimpleGraphObserver.java | 2 +- graph/README.md | 92 ++++++-- .../java/com/seaofnodes/graph/GraphCapture.java | 26 +++ .../main/java/com/seaofnodes/graph/GraphEvent.java | 16 +- .../main/java/com/seaofnodes/graph/GraphJson.java | 14 +- .../java/com/seaofnodes/graph/GraphObserver.java | 3 + .../java/com/seaofnodes/graph/GraphSnapshot.java | 7 +- graph/test_browser.py | 22 +- graph/test_transport.py | 2 +- graph/web/index.html | 12 +- graph/web/layout.js | 5 +- graph/web/peep.js | 197 +++++++++++++++++ graph/web/render.js | 140 ++++++++++-- graph/web/viewer.js | 234 ++++++++++++++++++--- 70 files changed, 951 insertions(+), 120 deletions(-) create mode 100644 graph/web/peep.js diff --git a/chapter10/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter10/src/main/java/com/seaofnodes/simple/node/StopNode.java index 9cc0e476..cb4126f5 100644 --- a/chapter10/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/chapter10/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.simple.Parser; + import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.IterPeeps; @@ -58,9 +60,15 @@ public class StopNode extends Node { } public StopNode iterate() { return IterPeeps.iterate(this).typeCheck(); } - StopNode typeCheck() { - String err = walk( Node::err ); + public StopNode typeCheck() { + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + String err = walk(n -> { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); return this; } } 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 3f4eeeba..1980659e 100644 --- a/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/chapter10/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -25,6 +25,8 @@ public class SimpleGraphObserver extends GraphCapture { _phase = "Iter"; com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); } @Override protected String phase() { return _phase; } @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } diff --git a/chapter11/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter11/src/main/java/com/seaofnodes/simple/node/StopNode.java index da32651a..fcd6d4ab 100644 --- a/chapter11/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/chapter11/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.simple.Parser; + import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.IterPeeps; @@ -68,9 +70,15 @@ public class StopNode extends CFGNode { } public StopNode iterate() { return IterPeeps.iterate(this).typeCheck(); } - StopNode typeCheck() { - String err = walk( Node::err ); + public StopNode typeCheck() { + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + String err = walk(n -> { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); return this; } 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 622d927f..feb9e90f 100644 --- a/chapter11/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/chapter11/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -25,6 +25,8 @@ public class SimpleGraphObserver extends GraphCapture { _phase = "Iter"; com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); } @Override protected String phase() { return _phase; } @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } diff --git a/chapter12/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter12/src/main/java/com/seaofnodes/simple/node/StopNode.java index c36bdf4d..3ef2e4a6 100644 --- a/chapter12/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/chapter12/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.simple.Parser; + import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.IterPeeps; @@ -68,9 +70,15 @@ public class StopNode extends CFGNode { } public StopNode iterate() { return IterPeeps.iterate(this).typeCheck(); } - StopNode typeCheck() { - String err = walk( Node::err ); + public StopNode typeCheck() { + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + String err = walk(n -> { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); return this; } 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 10ac4fdb..5c852f94 100644 --- a/chapter12/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/chapter12/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -25,6 +25,8 @@ public class SimpleGraphObserver extends GraphCapture { _phase = "Iter"; com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); } @Override protected String phase() { return _phase; } @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } diff --git a/chapter13/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter13/src/main/java/com/seaofnodes/simple/node/StopNode.java index aed4a8e6..af3a9de6 100644 --- a/chapter13/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/chapter13/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.simple.Parser; + import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.IterPeeps; import com.seaofnodes.simple.GlobalCodeMotion; @@ -69,9 +71,15 @@ public class StopNode extends CFGNode { } public StopNode iterate() { return IterPeeps.iterate(this).typeCheck().GCM(); } - StopNode typeCheck() { - String err = walk( Node::err ); + public StopNode typeCheck() { + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + String err = walk(n -> { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); return this; } StopNode GCM() { 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 c745dfaf..3aabd79f 100644 --- a/chapter13/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/chapter13/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -25,6 +25,8 @@ public class SimpleGraphObserver extends GraphCapture { _phase = "Iter"; com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); } @Override protected String phase() { return _phase; } @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } diff --git a/chapter14/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter14/src/main/java/com/seaofnodes/simple/node/StopNode.java index aed4a8e6..af3a9de6 100644 --- a/chapter14/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/chapter14/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.simple.Parser; + import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.IterPeeps; import com.seaofnodes.simple.GlobalCodeMotion; @@ -69,9 +71,15 @@ public class StopNode extends CFGNode { } public StopNode iterate() { return IterPeeps.iterate(this).typeCheck().GCM(); } - StopNode typeCheck() { - String err = walk( Node::err ); + public StopNode typeCheck() { + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + String err = walk(n -> { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); return this; } StopNode GCM() { 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 db9d4845..202aa9d9 100644 --- a/chapter14/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/chapter14/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -25,6 +25,8 @@ public class SimpleGraphObserver extends GraphCapture { _phase = "Iter"; com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); } @Override protected String phase() { return _phase; } @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } diff --git a/chapter15/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter15/src/main/java/com/seaofnodes/simple/node/StopNode.java index aed4a8e6..af3a9de6 100644 --- a/chapter15/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/chapter15/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.simple.Parser; + import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.IterPeeps; import com.seaofnodes.simple.GlobalCodeMotion; @@ -69,9 +71,15 @@ public class StopNode extends CFGNode { } public StopNode iterate() { return IterPeeps.iterate(this).typeCheck().GCM(); } - StopNode typeCheck() { - String err = walk( Node::err ); + public StopNode typeCheck() { + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + String err = walk(n -> { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); return this; } StopNode GCM() { 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 d3228b67..450922d7 100644 --- a/chapter15/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/chapter15/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -25,6 +25,8 @@ public class SimpleGraphObserver extends GraphCapture { _phase = "Iter"; com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); } @Override protected String phase() { return _phase; } @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } diff --git a/chapter16/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter16/src/main/java/com/seaofnodes/simple/node/StopNode.java index aed4a8e6..af3a9de6 100644 --- a/chapter16/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/chapter16/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -1,5 +1,7 @@ package com.seaofnodes.simple.node; +import com.seaofnodes.simple.Parser; + import com.seaofnodes.simple.type.Type; import com.seaofnodes.simple.IterPeeps; import com.seaofnodes.simple.GlobalCodeMotion; @@ -69,9 +71,15 @@ public class StopNode extends CFGNode { } public StopNode iterate() { return IterPeeps.iterate(this).typeCheck().GCM(); } - StopNode typeCheck() { - String err = walk( Node::err ); + public StopNode typeCheck() { + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + String err = walk(n -> { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); return this; } StopNode GCM() { 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 989a751b..0d6fce44 100644 --- a/chapter16/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/chapter16/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -25,6 +25,8 @@ public class SimpleGraphObserver extends GraphCapture { _phase = "Iter"; com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); } @Override protected String phase() { return _phase; } @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } diff --git a/chapter17/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter17/src/main/java/com/seaofnodes/simple/node/StopNode.java index fb8c3cc4..e498141e 100644 --- a/chapter17/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/chapter17/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -68,9 +68,15 @@ public class StopNode extends CFGNode { } public StopNode iterate() { return IterPeeps.iterate(this).typeCheck().GCM(); } - StopNode typeCheck() { - String err = walk( Node::err ); + public StopNode typeCheck() { + var obs = Parser.PARSER == null ? null : Parser.PARSER._obs; + String err = walk(n -> { + String msg = n.err(); + if( msg != null && obs != null ) obs.error(n, msg); + return msg; + }); if( err != null ) throw new RuntimeException(err); + if( obs != null ) obs.phase("TypeCheck"); return this; } StopNode GCM() { 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 38f84d7d..d774967b 100644 --- a/chapter17/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java +++ b/chapter17/src/main/java/com/seaofnodes/simple/print/SimpleGraphObserver.java @@ -25,6 +25,8 @@ public class SimpleGraphObserver extends GraphCapture { _phase = "Iter"; com.seaofnodes.simple.IterPeeps.iterate(_parser.STOP); phase(_phase); + _phase = "TypeCheck"; + _parser.STOP.typeCheck(); } @Override protected String phase() { return _phase; } @Override protected int pos() { return "Parse".equals(_phase) ? _parser.pos() : -1; } diff --git a/chapter18/src/main/java/com/seaofnodes/simple/CodeGen.java b/chapter18/src/main/java/com/seaofnodes/simple/CodeGen.java index fb646544..65d820f5 100644 --- a/chapter18/src/main/java/com/seaofnodes/simple/CodeGen.java +++ b/chapter18/src/main/java/com/seaofnodes/simple/CodeGen.java @@ -81,9 +81,14 @@ public class CodeGen { _phase = Phase.TypeCheck; // Type check - Parser.ParseException err = _stop.walk( Node::err ); + Parser.ParseException err = _stop.walk(n -> { + Parser.ParseException msg = n.err(); + if( msg != null && _obs != null ) _obs.error(n, msg.getMessage()); + return msg; + }); if( err != null ) throw err; + if( _obs != null ) _obs.phase("TypeCheck"); return this; } diff --git a/chapter18/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter18/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index 6d7aa323..54d76bbe 100644 --- a/chapter18/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/chapter18/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -14,6 +14,7 @@ public class CallEndNode extends CFGNode implements MultiNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined private boolean _folding; + public boolean folding() { return _folding; } public CallEndNode(CallNode call) { super(call); } diff --git a/chapter18/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter18/src/main/java/com/seaofnodes/simple/node/FunNode.java index 5873f6b6..ec60f215 100644 --- a/chapter18/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/chapter18/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -9,6 +9,7 @@ public class FunNode extends RegionNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined boolean _folding; + public boolean folding() { return _folding; } private TypeFunPtr _sig; // Initial signature private ReturnNode _ret; // Return pointer diff --git a/chapter18/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter18/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java index 17dd1131..fc06aeaa 100644 --- a/chapter18/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/chapter18/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -36,7 +36,17 @@ public class SimpleGraphAdapter extends GraphAdapter { : n instanceof CProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; String label = n.label(); return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, - n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj, folding(n)); + } + + private boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i { @Override protected void compile(String src) { _code = new CodeGen(src); _code._obs = this; - _code.parse().opto(); + _code.parse().opto().typeCheck(); } @Override protected String phase() { return _code._phase.name(); } @Override protected int pos() { return _code.P != null ? _code.P.pos() : -1; } diff --git a/chapter19/src/main/java/com/seaofnodes/simple/CodeGen.java b/chapter19/src/main/java/com/seaofnodes/simple/CodeGen.java index 8465d692..5baa05a6 100644 --- a/chapter19/src/main/java/com/seaofnodes/simple/CodeGen.java +++ b/chapter19/src/main/java/com/seaofnodes/simple/CodeGen.java @@ -169,9 +169,14 @@ public class CodeGen { assert _phase.ordinal() <= Phase.Opto.ordinal(); _phase = Phase.TypeCheck; - Parser.ParseException err = _stop.walk( Node::err ); + Parser.ParseException err = _stop.walk(n -> { + Parser.ParseException msg = n.err(); + if( msg != null && _obs != null ) _obs.error(n, msg.getMessage()); + return msg; + }); if( err != null ) throw err; + if( _obs != null ) _obs.phase("TypeCheck"); return this; } diff --git a/chapter19/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter19/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index e6aa9fe9..2205db9f 100644 --- a/chapter19/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/chapter19/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -11,6 +11,7 @@ public class CallEndNode extends CFGNode implements MultiNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined private boolean _folding; + public boolean folding() { return _folding; } public CallEndNode(CallNode call) { super(new Node[]{call}); } public CallEndNode(CallEndNode cend) { super(cend); } diff --git a/chapter19/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter19/src/main/java/com/seaofnodes/simple/node/FunNode.java index b514eba0..d24f4829 100644 --- a/chapter19/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/chapter19/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -10,6 +10,7 @@ public class FunNode extends RegionNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined boolean _folding; + public boolean folding() { return _folding; } private TypeFunPtr _sig; // Initial signature private ReturnNode _ret; // Return pointer diff --git a/chapter19/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter19/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java index ee424501..41725fcf 100644 --- a/chapter19/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/chapter19/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -36,7 +36,17 @@ public class SimpleGraphAdapter extends GraphAdapter { : n instanceof CProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; String label = n.label(); return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, - n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj, folding(n)); + } + + private boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i { @Override protected void compile(String src) { _code = new CodeGen(src); _code._obs = this; - _code.parse().opto(); + _code.parse().opto().typeCheck(); } @Override protected String phase() { return _code._phase.name(); } @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; } diff --git a/chapter20/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter20/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java index 5dca3953..93362799 100644 --- a/chapter20/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java +++ b/chapter20/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -176,10 +176,15 @@ public class CodeGen { _phase = Phase.TypeCheck; long t0 = System.currentTimeMillis(); - Parser.ParseException err = _stop.walk( Node::err ); + Parser.ParseException err = _stop.walk(n -> { + Parser.ParseException msg = n.err(); + if( msg != null && _obs != null ) _obs.error(n, msg.getMessage()); + return msg; + }); _tTypeCheck = (int)(System.currentTimeMillis() - t0); if( err != null ) throw err; + if( _obs != null ) _obs.phase("TypeCheck"); return this; } diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index 970045c5..c90e0530 100644 --- a/chapter20/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -12,6 +12,7 @@ public class CallEndNode extends CFGNode implements MultiNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined private boolean _folding; + public boolean folding() { return _folding; } public final TypeRPC _rpc; public CallEndNode(CallNode call) { super(new Node[]{call}); _rpc = TypeRPC.constant(_nid); } diff --git a/chapter20/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter20/src/main/java/com/seaofnodes/simple/node/FunNode.java index 99759bb0..2c91d1fc 100644 --- a/chapter20/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/chapter20/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -12,6 +12,7 @@ public class FunNode extends RegionNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined boolean _folding; + public boolean folding() { return _folding; } private TypeFunPtr _sig; // Initial signature private ReturnNode _ret; // Return pointer diff --git a/chapter20/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter20/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java index 4aae9acd..d0df57fa 100644 --- a/chapter20/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/chapter20/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -36,7 +36,17 @@ public class SimpleGraphAdapter extends GraphAdapter { : n instanceof CProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; String label = n.label(); return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, - n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj, folding(n)); + } + + private boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i { @Override protected void compile(String src) { _code = new CodeGen(src); _code._obs = this; - _code.parse().opto(); + _code.parse().opto().typeCheck(); } @Override protected String phase() { return _code._phase.name(); } @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; } diff --git a/chapter21/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter21/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java index 4bfd3c5d..064d5820 100644 --- a/chapter21/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java +++ b/chapter21/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -199,10 +199,15 @@ public class CodeGen { _phase = Phase.TypeCheck; long t0 = System.currentTimeMillis(); - Parser.ParseException err = _stop.walk( Node::err ); + Parser.ParseException err = _stop.walk(n -> { + Parser.ParseException msg = n.err(); + if( msg != null && _obs != null ) _obs.error(n, msg.getMessage()); + return msg; + }); _tTypeCheck = (int)(System.currentTimeMillis() - t0); if( err != null ) throw err; + if( _obs != null ) _obs.phase("TypeCheck"); return this; } diff --git a/chapter21/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter21/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index f99a0b89..3005344d 100644 --- a/chapter21/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/chapter21/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -12,6 +12,7 @@ public class CallEndNode extends CFGNode implements MultiNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined private boolean _folding; + public boolean folding() { return _folding; } public final TypeRPC _rpc; public CallEndNode(CallNode call) { super(new Node[]{call}); _rpc = TypeRPC.constant(_nid); } diff --git a/chapter21/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter21/src/main/java/com/seaofnodes/simple/node/FunNode.java index 384547fa..eb1e8259 100644 --- a/chapter21/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/chapter21/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -15,6 +15,7 @@ public class FunNode extends RegionNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined boolean _folding; + public boolean folding() { return _folding; } private TypeFunPtr _sig; // Initial signature private ReturnNode _ret; // Return pointer diff --git a/chapter21/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter21/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java index 6721c0b7..d6dc6ae1 100644 --- a/chapter21/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/chapter21/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -36,7 +36,17 @@ public class SimpleGraphAdapter extends GraphAdapter { : n instanceof CProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; String label = n.label(); return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, - n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj, folding(n)); + } + + private boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i { @Override protected void compile(String src) { _code = new CodeGen(src); _code._obs = this; - _code.parse().opto(); + _code.parse().opto().typeCheck(); } @Override protected String phase() { return _code._phase.name(); } @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; } diff --git a/chapter22/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter22/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java index bdb9c5cc..0c00b9c6 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -220,10 +220,15 @@ public class CodeGen { _phase = Phase.TypeCheck; long t0 = System.currentTimeMillis(); - Parser.ParseException err = _stop.walk( Node::err ); + Parser.ParseException err = _stop.walk(n -> { + Parser.ParseException msg = n.err(); + if( msg != null && _obs != null ) _obs.error(n, msg.getMessage()); + return msg; + }); _tTypeCheck = (int)(System.currentTimeMillis() - t0); if( err != null ) throw err; + if( _obs != null ) _obs.phase("TypeCheck"); return this; } diff --git a/chapter22/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter22/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index cf3f337c..1b4b619a 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -12,6 +12,7 @@ public class CallEndNode extends CFGNode implements MultiNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined private boolean _folding; + public boolean folding() { return _folding; } public final TypeRPC _rpc; public CallEndNode(CallNode call) { super(new Node[]{call}); _rpc = TypeRPC.constant(_nid); } diff --git a/chapter22/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter22/src/main/java/com/seaofnodes/simple/node/FunNode.java index c53807a6..9afbf77e 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -15,6 +15,7 @@ public class FunNode extends RegionNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined boolean _folding; + public boolean folding() { return _folding; } private TypeFunPtr _sig; // Initial signature private ReturnNode _ret; // Return pointer diff --git a/chapter22/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter22/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java index ac7d2f15..d4906a1b 100644 --- a/chapter22/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/chapter22/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -36,7 +36,17 @@ public class SimpleGraphAdapter extends GraphAdapter { : n instanceof CProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; String label = n.label(); return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, - n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj, folding(n)); + } + + private boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i { @Override protected void compile(String src) { _code = new CodeGen(src); _code._obs = this; - _code.parse().opto(); + _code.parse().opto().typeCheck(); } @Override protected String phase() { return _code._phase.name(); } @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; } diff --git a/chapter23/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter23/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java index ca0e1110..adfefae5 100644 --- a/chapter23/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java +++ b/chapter23/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -242,10 +242,15 @@ public class CodeGen { _phase = Phase.TypeCheck; long t0 = System.currentTimeMillis(); - Parser.ParseException err = _stop.walk( Node::err ); + Parser.ParseException err = _stop.walk(n -> { + Parser.ParseException msg = n.err(); + if( msg != null && _obs != null ) _obs.error(n, msg.getMessage()); + return msg; + }); _tTypeCheck = (int)(System.currentTimeMillis() - t0); if( err != null ) throw err; + if( _obs != null ) _obs.phase("TypeCheck"); return this; } diff --git a/chapter23/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter23/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index d95af697..1b702bee 100644 --- a/chapter23/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/chapter23/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -12,6 +12,7 @@ public class CallEndNode extends CFGNode implements MultiNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined private boolean _folding; + public boolean folding() { return _folding; } public final TypeRPC _rpc; public CallEndNode(CallNode call) { super(new Node[]{call}); _rpc = TypeRPC.constant(_nid); } diff --git a/chapter23/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter23/src/main/java/com/seaofnodes/simple/node/FunNode.java index 3ef486f8..88f5dc18 100644 --- a/chapter23/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/chapter23/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -16,6 +16,7 @@ public class FunNode extends RegionNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined boolean _folding; + public boolean folding() { return _folding; } private TypeFunPtr _sig; // Initial signature private ReturnNode _ret; // Return pointer diff --git a/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java index daae6fb6..7f7107ec 100644 --- a/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/chapter23/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -36,7 +36,17 @@ public class SimpleGraphAdapter extends GraphAdapter { : n instanceof CProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; String label = n.label(); return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, - n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj, folding(n)); + } + + private boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i { @Override protected void compile(String src) { _code = new CodeGen(src); _code._obs = this; - _code.parse().opto(); + _code.parse().opto().typeCheck(); } @Override protected String phase() { return _code._phase.name(); } @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; } diff --git a/chapter24/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter24/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java index d4af3a10..274fb2c7 100644 --- a/chapter24/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java +++ b/chapter24/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -210,10 +210,15 @@ public class CodeGen { _phase = Phase.TypeCheck; long t0 = System.currentTimeMillis(); - Parser.ParseException err = _stop.walk( Node::err ); + Parser.ParseException err = _stop.walk(n -> { + Parser.ParseException msg = n.err(); + if( msg != null && _obs != null ) _obs.error(n, msg.getMessage()); + return msg; + }); _tTypeCheck = (int)(System.currentTimeMillis() - t0); if( err != null ) throw err; + if( _obs != null ) _obs.phase("TypeCheck"); return this; } diff --git a/chapter24/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter24/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index 9cf6c641..238f5582 100644 --- a/chapter24/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/chapter24/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -12,6 +12,7 @@ public class CallEndNode extends CFGNode implements MultiNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined private boolean _folding; + public boolean folding() { return _folding; } public final TypeRPC _rpc; public CallEndNode(CallNode call) { super(new Node[]{call}); _rpc = TypeRPC.constant(_nid); } diff --git a/chapter24/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter24/src/main/java/com/seaofnodes/simple/node/FunNode.java index a413689f..508dc63d 100644 --- a/chapter24/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/chapter24/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -16,6 +16,7 @@ public class FunNode extends RegionNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined boolean _folding; + public boolean folding() { return _folding; } private TypeFunPtr _sig; // Initial signature private ReturnNode _ret; // Return pointer diff --git a/chapter24/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java b/chapter24/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java index c14f2d3a..c895a0cb 100644 --- a/chapter24/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/chapter24/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -36,7 +36,17 @@ public class SimpleGraphAdapter extends GraphAdapter { : n instanceof CProjNode p ? new Projection(ref(n.in(0)), p._idx) : null; String label = n.label(); return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, - n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj, folding(n)); + } + + private boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i { @Override protected void compile(String src) { _code = new CodeGen(src); _code._obs = this; - _code.parse().opto(); + _code.parse().opto().typeCheck(); } @Override protected String phase() { return _code._phase.name(); } @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; } diff --git a/chapter25/pom.xml b/chapter25/pom.xml index 7a2b2b78..ab9f2ea5 100644 --- a/chapter25/pom.xml +++ b/chapter25/pom.xml @@ -8,9 +8,9 @@ simple 1.0 - chapter23 + chapter25 jar - Chapter 23 + Chapter 25 ${project.basedir}/../graph diff --git a/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java index 87aa96c4..0f38d3ff 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java @@ -558,10 +558,15 @@ public class CodeGen { min instanceof CFGNode mincfg && ncfg.idepth() < mincfg.idepth())) ) min = n; + if( _obs != null ) _obs.error(min, min.err().getMessage()); throw min.err(); } - if( unresolved != null ) - throw Parser.error("Unknown struct type '"+unresolved+"'",null); + if( unresolved != null ) { + var err = Parser.error("Unknown struct type '"+unresolved+"'",null); + if( _obs != null ) _obs.error(null, err.getMessage()); + throw err; + } + if( _obs != null ) _obs.phase("TypeCheck"); return this; } diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/CallEndNode.java index abf80288..3c6506b9 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/node/CallEndNode.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/node/CallEndNode.java @@ -56,7 +56,7 @@ public class CallEndNode extends CFGNode implements MultiNode { @Override public boolean blockHead() { return true; } public CallNode call() { return (CallNode)in(0); } - boolean folding() { return _folding; } + public boolean folding() { return _folding; } @Override public CFGNode idom(Node dep) { // Folding the idom is the one inlining Return diff --git a/chapter25/src/main/java/com/seaofnodes/simple/node/FunNode.java b/chapter25/src/main/java/com/seaofnodes/simple/node/FunNode.java index ddd637c9..19b459df 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/node/FunNode.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/node/FunNode.java @@ -15,6 +15,7 @@ public class FunNode extends RegionNode { // When set true, this Call/CallEnd/Fun/Return is being trivially inlined public boolean _folding; + public boolean folding() { return _folding; } private TypeFunPtr _sig; // Initial signature private ReturnNode _ret; // Return pointer 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 f3619bb5..6c4e7e31 100644 --- a/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java +++ b/chapter25/src/main/java/com/seaofnodes/simple/print/SimpleGraphAdapter.java @@ -44,7 +44,17 @@ public class SimpleGraphAdapter extends GraphAdapter { ? new Projection(n.nIns() == 0 ? 0 : ref(n.in(0)), p.idx()) : null; String label = n.label(); return new GraphSnapshot.Node(n._nid, label == null ? n.getClass().getSimpleName() : label, - n._type == null ? null : n._type.toString(), kind(n), edges(n), proj); + n._type == null ? null : n._type.toString(), kind(n), edges(n), proj, folding(n)); + } + + private boolean folding(Node n) { + if( n instanceof FunNode fun ) return fun.folding(); + if( n instanceof ReturnNode ret ) return ret.fun().folding(); + if( n instanceof CallEndNode cend ) return cend.folding(); + if( n instanceof CallNode ) + for( int i=0; i { @Override protected void compile(String src) { _code = new CodeGen(src); _code._obs = this; - _code.parse().opto(); + _code.parse().opto().typeCheck(); } @Override protected String phase() { return _code._phase.name(); } @Override protected int pos() { return _code._phase == CodeGen.Phase.Parse ? _code.P.pos() : -1; } diff --git a/graph/README.md b/graph/README.md index 27845b46..e79ff9e5 100644 --- a/graph/README.md +++ b/graph/README.md @@ -3,7 +3,8 @@ The browser application lives in `web/` and is shared by chapters 1–25. `web/index.html` contains the page and `web/viewer.js` manages playback. `web/groups.js` projects folded regions into display nodes; `web/layout.js` -runs ELK layout in a browser worker; `web/render.js` draws SVG +runs ELK layout in a browser worker; `web/peep.js` builds temporary neighborhoods; +`web/render.js` draws SVG and handles selection, pan and zoom. Libraries live in `web/vendor/`, with one checked-in copy of each; chapters do not copy them during builds. @@ -29,13 +30,63 @@ The compiler WebSocket still uses port 12345; run one viewer session at a time. The initial program compiles on connection. For another program, click **Compile** or press **Ctrl+Enter** (**Cmd+Enter** on macOS). Use the arrows to step through -frames, the first/last buttons to jump to either end, or enter a frame number +frames (Left/Right arrow keys perform the same steps, including animation stages), +the first/last buttons to jump to either end, or enter a frame number and press **Enter**. The slider scrubs through available frames; its arrow keys -step and **Home/End** jump to either end. Keep `make view` running while using the tab. +step and **Home/End** jump to either end. Inputs keep their own arrow-key behavior; +the playback shortcuts apply outside inputs, without modifier keys. +Keep `make view` running while using the tab. Frame numbers start at 1. Jumps lay out only their destination. When the slider moves during layout, the viewer finishes that calculation and then handles the latest request, skipping intermediate requests. Already visited layouts stay cached. +Chapters 10–25 run their existing TypeCheck pass after optimization. A successful +check adds a TypeCheck phase-complete frame. An error adds a final graph frame +with the compiler's diagnostic; stepping or jumping to it centers the error node +at a readable scale, outlines it in red, and shows the message on the phase line +and in node details. Folded regions containing that node are revealed for this +frame without changing earlier frames' fold choices. The graph and history stay +available. Errors without an associated node still show their message. + +Structural peepholes take three forward clicks: **gather**, **rewrite**, then +**release**. Gathering slides the edit and its context into a highlighted enclosure. +The rewrite adds/removes nodes and reconnects edges there; surviving nodes keep +their positions. Release restores the ordinary post-peep graph and the previous +view as closely as possible. Backward clicks reverse these stages. Type-only +updates and unchanged results remain one-click steps without gathering or zooming. + +Call, CallEnd, Fun and Return nodes being folded away by inlining have a small +**FOLDING** tag and a pink fill. This reflects the compiler's folding flag, +including between peeps, and is separate from the viewer's region fold buttons. + +The enclosure includes nodes changed between the snapshots, the peep root and +replacement, and their direct defs in either snapshot, excluding constants' Start +anchors. An existing GVN replacement is gathered alongside the original even when +the replacement itself is unchanged. Defs do not recursively bring their own +inputs; recorded scheduling dependencies do +not expand the enclosure. It reserves space for both versions, keeps Multi/projection boxes +intact, and places gathered Phis beside their Region without pulling in sibling +Phis. Unrelated graph content is subdued. Affected folded +regions are temporarily revealed; release restores the user's folds. Nodes one +edge outside it remain more visible than the distant graph. Playback pairs an +enclosing peep's BEFORE with its matching RETURN/APPLY, treating recursive peeps +as part of that edit. A replacement built and typed by a nested peep therefore +appears on the rewrite click, never before gathering its parent's neighborhood. +When a RETURN precedes the caller attaching its replacement, playback can use +the next capture as the result: every intervening node change must be a use +switching from the old root to the replacement, or deletion of the root and its +unused inputs. This shows the actual rewiring/removal in the enclosure. Unrelated +parser edits are not combined. The following BEFORE can serve both as this +edit's result and as the next edit's starting point. + +Gathered/rewritten views are intermediate states, identified in the event line. +Frame numbers, the slider, and first/last buttons always jump directly to stable +snapshots. Capture numbers are retained, so stepping can skip numbers belonging +to nested attempts or a RETURN waiting for its caller's attachment. Entering or +scrubbing to one of those numbers lands on the enclosing BEFORE frame. +A jump or Escape cancels an intermediate view. Reduced-motion mode +keeps the same inspectable stages but changes views immediately. + Functions, loops and closed If/Region diamonds have enclosing boxes. Click **−** in a box's upper-left corner to fold it into a thick-bordered node; click **+** to reopen it. **Unfold all** restores the complete graph. Folds persist while stepping forward @@ -60,8 +111,8 @@ mouse away, panning, zooming, Escape, Fit, or changing frames cancels the jump. The pan is immediate when the browser requests reduced motion. **Peep neighborhood** outlines nearby nodes in amber, with a darker outline on the current node/replacement. The event label identifies the phase, rewrite, -and enclosing rewrite when calls nest. This marks context; it does not zoom or -animate the rewrite yet. +and enclosing rewrite when calls nest. This checkbox controls the node outlines; +the enclosure remains visible throughout the intermediate animation stages. For documentation figures, step to the desired frame and click **Save SVG**. It saves the whole graph, regardless of pan/zoom, with its current association @@ -137,7 +188,7 @@ The JSON frame has this shape: "pos": -1, "evt": { "kind": "PHASE", "peep": 0, "up": 0, "phase": "Opto", - "node": 0, "repl": 0, "near": [] + "node": 0, "repl": 0, "near": [], "temps": [] } } ``` @@ -146,7 +197,8 @@ The JSON frame has this shape: adds the 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 +their names; absent types, labels and projections use JSON null. Optional +`folding: true` marks nodes awaiting inline cleanup; omission means false. 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, @@ -155,7 +207,8 @@ 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 uses exactly that geometry. Viewport changes do not rerun layout. Nodes and -edges have stable SVG IDs within a compilation. There is no animation yet. +edges have stable SVG IDs within a compilation. Temporary peep geometry is +separate from these cached stable layouts. Each frame also caches its most recent folded layout, keyed by the folded header IDs; changing folds recomputes that view without changing the snapshot. One `+` acknowledges the whole frame. The shared writer sends UTF-8 bytes @@ -213,6 +266,9 @@ 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. +Nodes created inside an enclosing peephole are excluded from these Parser arrows +until that outer attempt returns; a nested constant fold can create its result +before the parent replaces the arithmetic node. Function, loop and diamond membership comes from the compiler process; ELK lays out the supplied hierarchy. RPO is not a pending layout requirement. @@ -224,8 +280,8 @@ Pending graph work: - Refine CFG placement with compilation-unit containers and fuller membership for unfinished loops and paths leaving loops. -- Preserve positions across peepholes, then animate edits and zoom into the - peep neighborhood before returning to the whole graph. +- Improve stable whole-graph placement across peepholes. Temporary neighborhoods + already preserve positions across each edit, animate it and return to the graph. - For large compilations, add checkpoints and forward deltas with a bounded cache of recent backward steps. @@ -401,7 +457,8 @@ The event kinds distinguish the actual capture boundaries: | `BEFORE` | Graph before an attempt that made progress, or contained a nested rewrite | | `RETURN` | Recursive `Node.peephole()` finished, including its local DCE; the caller has not yet attached the returned node | | `APPLY` | A worklist attempt finished after substitution, dependency draining, and unused-node cleanup | -| `PHASE` | Parse, Iter, or Opto completed, including work outside individual peepholes | +| `PHASE` | Parse, Iter, Opto, or TypeCheck completed, including work outside individual peepholes | +| `ERROR` | TypeCheck failed; `node` identifies the offending node (zero if unavailable), and `msg` contains the diagnostic | `peep` identifies an attempt; `up` identifies its enclosing attempt, or zero. `node` and `repl` identify the original and result nodes. A zero `repl` on @@ -421,6 +478,12 @@ not yet attached by the parser remain visible. Dead originals are not added as after-frame roots. The observer never adds keep edges, changes use lists, or modifies the compiler's dependency tracking. +`temps` lists nodes created since the outermost active peep's before snapshot, +while a nested peep is being shown. They have not returned to the parser yet. +The list is empty on outermost results and phase events. This distinguishes +userless peep temporaries from parser-held expression values without adding +compiler edges or inferring ownership from the current result node alone. + `near` is a primitive array of node IDs, assembled with a `BitSet`. It combines: - The current node and replacement, with their immediate defs and uses before @@ -436,11 +499,12 @@ describe an entire successful pattern match. Some rules inspect distant nodes without registering a dependency. Before events include context known when emitted; their matching after events can add more. IDs of deleted nodes remain in `near`, and the browser highlights only those present in the current frame. -Actual graph diffs and explicit rule annotations can refine this later. +The animation uses graph diffs to include every changed node in the enclosure. The browser keeps the event alongside its snapshot and layout. It labels and -highlights the current step, but does not yet collapse nested rewrites or move -the camera to their neighborhoods. Chapter 4 uses the same event model, with +highlights the current step and gathers each enclosing rewrite's neighborhood. +Nested captures remain available in the stream, but playback presents their net +effect as part of the enclosing edit. Chapter 4 uses the same event model, with defs/uses for its neighborhoods and no worklist or dependency lists. It retains the final Return as a root after scope cleanup. The compiler DOT dump flag, parser graph directive, Java DOT generators and old diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphCapture.java b/graph/src/main/java/com/seaofnodes/graph/GraphCapture.java index 1a9c8c62..966dd701 100644 --- a/graph/src/main/java/com/seaofnodes/graph/GraphCapture.java +++ b/graph/src/main/java/com/seaofnodes/graph/GraphCapture.java @@ -12,8 +12,10 @@ public abstract class GraphCapture extends GraphObserver { private String _comp; private GraphSocket _server; private final ArrayList _peeps = new ArrayList<>(); + private final BitSet _pre = new BitSet(); private long _next, _step; private boolean _off = true; + private boolean _err; protected GraphCapture(GraphAdapter graph) { _graph = graph; } @@ -32,8 +34,13 @@ public abstract class GraphCapture extends GraphObserver { _next = _step = 0; _server = server; _off = false; + _err = false; try { compile(src); + } catch( RuntimeException e ) { + // Type checking already emitted its diagnostic with the final graph. + if( !_err ) throw e; + System.err.println(e); } finally { _off = true; try { @@ -68,6 +75,10 @@ public abstract class GraphCapture extends GraphObserver { // 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, true); + if( p.up == 0 ) { + _pre.clear(); + for( var node : p.pre.nodes() ) _pre.set(node.id()); + } } @Override public void after(N n, N repl, boolean applied) { @@ -106,6 +117,13 @@ public abstract class GraphCapture extends GraphObserver { emit(snap(null, false), pos(), new GraphEvent(GraphEvent.Kind.PHASE, 0, 0, phase, 0, 0, new int[0])); } + @Override public void error(N node, String msg) { + if( _off ) return; + _err = true; + emit(snap(node, false), pos(), new GraphEvent(GraphEvent.Kind.ERROR, 0, 0, + phase(), _graph.ref(node), 0, new int[0], new int[0], msg)); + } + private GraphEvent evt(Peep p, GraphEvent.Kind kind, int repl) { return new GraphEvent(kind, p.id, p.up, p.phase, _graph.id(p.node), repl, p.near.stream().toArray()); } @@ -137,6 +155,14 @@ public abstract class GraphCapture extends GraphObserver { private void emit(GraphSnapshot snap, int pos, GraphEvent evt) { if( _off ) return; + if( evt.up() != 0 ) { + // Nested peepholes can create userless results before their caller + // returns. These belong to the peep, not the parser's expression. + int[] temps = snap.nodes().stream().mapToInt(GraphSnapshot.Node::id) + .filter(id -> !_pre.get(id)).toArray(); + evt = new GraphEvent(evt.kind(), evt.peep(), evt.up(), evt.phase(), + evt.node(), evt.repl(), evt.near(), temps); + } // Assign steps on emission; attempts without progress do not consume frames. snap = new GraphSnapshot(snap.ver(), _comp, _step++, snap.roots(), snap.scope(), snap.nodes(), snap.groups()); try { diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphEvent.java b/graph/src/main/java/com/seaofnodes/graph/GraphEvent.java index 7d889b21..d52213c2 100644 --- a/graph/src/main/java/com/seaofnodes/graph/GraphEvent.java +++ b/graph/src/main/java/com/seaofnodes/graph/GraphEvent.java @@ -2,9 +2,19 @@ package com.seaofnodes.graph; /** IDs refer to nodes in this compilation; near may include nodes since deleted. */ public record GraphEvent(Kind kind, long peep, long up, String phase, - int node, int repl, int[] near) { - public enum Kind { BEFORE, RETURN, APPLY, PHASE } + int node, int repl, int[] near, int[] temps, String msg) { + public enum Kind { BEFORE, RETURN, APPLY, PHASE, ERROR } - public GraphEvent { near = near.clone(); } + public GraphEvent(Kind kind, long peep, long up, String phase, int node, int repl, int[] near, int[] temps) { + this(kind, peep, up, phase, node, repl, near, temps, null); + } + + public GraphEvent(Kind kind, long peep, long up, String phase, int node, int repl, int[] near) { + this(kind, peep, up, phase, node, repl, near, new int[0]); + } + + public GraphEvent { near = near.clone(); temps = temps.clone(); } @Override public int[] near() { return near.clone(); } + // Created inside an enclosing peep; not yet returned to the parser. + @Override public int[] temps() { return temps.clone(); } } diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphJson.java b/graph/src/main/java/com/seaofnodes/graph/GraphJson.java index 004f6827..bcdd46cc 100644 --- a/graph/src/main/java/com/seaofnodes/graph/GraphJson.java +++ b/graph/src/main/java/com/seaofnodes/graph/GraphJson.java @@ -29,7 +29,18 @@ public class GraphJson { if( i > 0 ) sb.append(','); sb.append(near[i]); } - sb.append("]}"); + sb.append("],\"temps\":["); + int[] temps = evt.temps(); + for( int i = 0; i < temps.length; i++ ) { + if( i > 0 ) sb.append(','); + sb.append(temps[i]); + } + sb.append(']'); + if( evt.msg() != null ) { + sb.append(",\"msg\":"); + str(sb, evt.msg()); + } + sb.append('}'); return sb.append('}').toString(); } @@ -53,6 +64,7 @@ public class GraphJson { str(sb, node.type()); sb.append(",\"kind\":"); str(sb, node.kind().name()); + if( node.folding() ) sb.append(",\"folding\":true"); sb.append(",\"edges\":["); var edges = node.edges(); for( int j = 0; j < edges.size(); j++ ) { diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphObserver.java b/graph/src/main/java/com/seaofnodes/graph/GraphObserver.java index 24879fe2..4a57c816 100644 --- a/graph/src/main/java/com/seaofnodes/graph/GraphObserver.java +++ b/graph/src/main/java/com/seaofnodes/graph/GraphObserver.java @@ -14,6 +14,9 @@ public abstract class GraphObserver { /** A completed compiler phase. */ public abstract void phase(String phase); + /** A compiler diagnostic and its node, or null for an error without a node. */ + public void error(N node, String msg) {} + /** A rule consulted a dependency, even if already registered or adjacent. */ public void dep(N node, N def) {} } diff --git a/graph/src/main/java/com/seaofnodes/graph/GraphSnapshot.java b/graph/src/main/java/com/seaofnodes/graph/GraphSnapshot.java index b53f3211..df1e8837 100644 --- a/graph/src/main/java/com/seaofnodes/graph/GraphSnapshot.java +++ b/graph/src/main/java/com/seaofnodes/graph/GraphSnapshot.java @@ -21,9 +21,12 @@ public record GraphSnapshot(int ver, String comp, long step, /** Header ID, enclosing group (zero outside), and directly owned node IDs. */ public record Group(int id, int par, int[] nodes) { } - /** IDs are stable within one compilation. Labels and types are plain text. */ + /** IDs are stable; labels/types are plain text. folding means pending inline cleanup. */ public record Node(int id, String label, String type, Kind kind, - ArrayList edges, Projection proj) { + ArrayList edges, Projection proj, boolean folding) { + public Node(int id, String label, String type, Kind kind, ArrayList edges, Projection proj) { + this(id,label,type,kind,edges,proj,false); + } public Node { edges = new ArrayList<>(edges); } diff --git a/graph/test_browser.py b/graph/test_browser.py index 52cb5fa6..aa1015e1 100644 --- a/graph/test_browser.py +++ b/graph/test_browser.py @@ -45,14 +45,23 @@ def check(chapter, browser_name): page = browser.new_page(viewport={"width": 1400, "height": 1000}) errors = [] page.on("pageerror", lambda error: errors.append(str(error))) + def step(button): + # Structural edits have gather/rewrite/release stages; + # this smoke check visits the next stable snapshot. + for _ in range(3): + page.locator(button).click() + page.wait_for_function("!rendering && !stepping") + if page.evaluate("!peep"): + return + raise AssertionError("Peephole did not reach a stable frame") page.goto(url) page.wait_for_function("done && current === 0 && !rendering", timeout=15000) assert page.locator("#graph svg g.node").count() > 0 assert page.locator("#doPrev").is_disabled() if number > 1: - page.locator("#doNext").click() + step("#doNext") page.wait_for_function("current === 1 && !rendering") - page.locator("#doPrev").click() + step("#doPrev") page.wait_for_function("current === 0 && !rendering") sources = (("return 1;", "return 2;") if number == 1 else ("return 1 + 2;", "return 3 * 4;") if number == 2 else @@ -70,11 +79,12 @@ def check(chapter, browser_name): # Later chapters also parse library code. Sample long histories. steps = (range(1, total) if total <= 100 else sorted({1, total // 4, total // 2, 3 * total // 4, total - 2, total - 1})) + steps = sorted(set(page.evaluate("xs => xs.map(stable)", list(steps))) - {0}) print(chapter, total, "frames captured; rendering", len(steps) + 1, flush=True) prev = 0 for index in steps: - if index == prev + 1: - page.locator("#doNext").click() + if index == page.evaluate("i => nextFrame(i,1)", prev): + step("#doNext") else: page.evaluate("index => render(index)", index) page.wait_for_function("index => current === index && !rendering", arg=index) @@ -90,9 +100,9 @@ def check(chapter, browser_name): renderer.select(pickId); layout.run = () => { throw new Error('Cached frame was laid out again'); }; }""") - page.locator("#doPrev").click() + step("#doPrev") page.wait_for_function("index => current === index && !rendering", arg=total - 2) - page.locator("#doNext").click() + step("#doNext") page.wait_for_function("index => current === index && !rendering", arg=total - 1) assert page.evaluate("frames[current].layout === lastLayout && renderer.pick === pickId") page.evaluate("delete layout.run") diff --git a/graph/test_transport.py b/graph/test_transport.py index f1834403..c4b8a4fe 100644 --- a/graph/test_transport.py +++ b/graph/test_transport.py @@ -69,7 +69,7 @@ def check(chapter): time.sleep(.05) assert url and url.startswith("http://127.0.0.1:"), log.read_text() # The browser must be able to load all assets before connecting. - for asset in ("index.html", "viewer.js", "layout.js", "render.js", + for asset in ("index.html", "viewer.js", "layout.js", "peep.js", "render.js", "vendor/elk-api.js", "vendor/elk-worker.min.js", "vendor/d3.v7.min.js"): with urllib.request.urlopen(url.rsplit("/", 1)[0] + "/" + asset, timeout=3) as response: diff --git a/graph/web/index.html b/graph/web/index.html index 4a32b461..7e66fe5a 100644 --- a/graph/web/index.html +++ b/graph/web/index.html @@ -33,6 +33,13 @@ .node.focus .box { stroke:#9c4900; stroke-width:3; } .node.selected .box { stroke:#1569b9; stroke-width:3; } .node.folded .box { stroke-width:4; } + .node.folding .box { fill:#f4ccd7; } + .fold-tag { font-size:9px; font-weight:700; fill:#963751; } + .peep-area { fill:#fff3d9; fill-opacity:.8; stroke:#c18221; stroke-width:2; pointer-events:none; } + .node.peep-edit .box { stroke:#9c4900; stroke-width:3; } + .node.error .box { stroke:#b42318; stroke-width:4; } + #event.error { color:#b42318; font-weight:600; white-space:pre-wrap; overflow-wrap:anywhere; } + .peep-ghosts { pointer-events:none; } .region-box { stroke:#9da9b8; stroke-width:1.5; } .fold-btn { cursor:pointer; } .fold-btn rect { fill:white; stroke:#596674; } @@ -64,6 +71,7 @@ + @@ -87,12 +95,12 @@
- + / 0 - + diff --git a/graph/web/layout.js b/graph/web/layout.js index 8260fbd2..880e7662 100644 --- a/graph/web/layout.js +++ b/graph/web/layout.js @@ -25,7 +25,7 @@ class GraphLayout { const nodes = snap.nodes.map(n => { const label = `${this.clip(n.label)} #${n.id}${n.proj ? "/" + n.proj.idx : ""}`, type = this.clip(n.type); const compact = ["START", "UNIT", "STOP"].includes(n.kind); - let 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 + (n.folding ? 60 : 0), this.ctx.measureText(type).width + 24, (n.edges.length + 1) * 18)); const scope = n.kind === "SCOPE"; const cols = scope ? n.edges.map(e => Math.ceil(Math.max(48, @@ -49,8 +49,9 @@ class GraphLayout { // 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 temps = new Set(evt?.temps); const held = parsing ? nodes.filter(n => - (n.n.proj || ["DATA", "MEM", "PHI"].includes(n.n.kind)) && !used.has(n.n.id)) : []; + (n.n.proj || ["DATA", "MEM", "PHI"].includes(n.n.kind)) && !used.has(n.n.id) && !temps.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(); diff --git a/graph/web/peep.js b/graph/web/peep.js new file mode 100644 index 00000000..2cf1ccf2 --- /dev/null +++ b/graph/web/peep.js @@ -0,0 +1,197 @@ +// Temporary geometry for one edit. Stable snapshots and their layouts are untouched. +class GraphPeep { + // A recursive peephole can return a replacement before its caller attaches + // it. Accept the next snapshot only when every intervening graph change is + // that substitution or deletion of its now-unused inputs. + static attached(a, b, evt) { + const pre = new Map(a.nodes.map(n => [n.id,n])); + const post = new Map(b.nodes.map(n => [n.id,n])); + if (!pre.has(evt.node) || !pre.has(evt.repl) || !post.has(evt.repl)) return false; + if (b.nodes.some(n => !pre.has(n.id))) return false; + const gone = new Set(); + let rewires = 0; + for (const n of a.nodes) { + const m = post.get(n.id); + if (!m) { gone.add(n.id); continue; } + if (n.kind !== m.kind || n.label !== m.label || n.type !== m.type || !!n.folding !== !!m.folding || + JSON.stringify(n.proj) !== JSON.stringify(m.proj) || n.edges.length !== m.edges.length) return false; + for (let i=0; i 0 && !gone.size; + } + + static diff(a, b, evt) { + const pre = new Map(a.nodes.map(n => [n.id, n])); + const post = new Map(b.nodes.map(n => [n.id, n])); + const changed = new Set(); + let structural = false; + for (const id of new Set([...pre.keys(), ...post.keys()])) { + const n = pre.get(id), m = post.get(id); + const shape = n => n && JSON.stringify([n.kind, n.proj, !!n.folding, n.edges.map(e => [e.idx,e.def])]); + const edit = shape(n) !== shape(m); + if (!edit && n.type === m.type && n.label === m.label && JSON.stringify(n.edges) === JSON.stringify(m.edges)) continue; + changed.add(id); + if (edit) structural = true; + } + const core = new Set([...changed, evt.node]); + // GVN can return an unchanged node that was already in the graph. Show it + // alongside the original, even when it isn't a def of any changed node. + if (evt.repl) core.add(evt.repl); + const near = new Set(core); + // One hop from changed nodes and both peep roots, in both snapshots. Do + // not expand from the added defs or scheduling dependencies. + for (const ns of [pre,post]) for (const id of core) { + const n = ns.get(id); + if (!n) continue; + const constant = n.kind !== "SCOPE" && !n.proj && n.edges.every(e => e.role === "ASSOC"); + for (const e of n.edges) { + const def = ns.get(e.def); + if (!def) continue; + // Early chapters still describe Start as CTRL rather than START. + if (constant && (def.kind === "START" || def.label === "Start")) continue; + near.add(e.def); + } + } + near.delete(0); + return {structural, changed, near}; + } + + static async make(pre, post, diff, layout, folded, sceneFor, evt = post.evt) { + const folds = new Set(folded); + // Reveal the edit even when its function/loop is folded. Release restores + // the user's folds; gathering never changes the persistent fold set. + for (const frame of [pre, post]) { + const view = GraphGroups.view(frame.snap, new Set()); + for (const id of folds) + if ([...(view.groups.get(id)?.members || [])].some(n => diff.near.has(n))) folds.delete(id); + } + const a = await sceneFor(pre, folds), b = await sceneFor(post, folds); + const ids = new Set(diff.near); + const nodes = new Map(); + for (const scene of [a, b]) for (const n of scene.nodes) nodes.set(n.n.id, n); + // Multi/projection cells travel as one box. A gathered Phi brings its + // Region for the slot-0 marker, but a Region does not bring sibling Phis. + let more = true; + while (more) { + more = false; + for (const scene of [a, b]) for (const n of scene.nodes) { + const par = n.par || n.reg; + if (!par || !(ids.has(n.n.id) || n.par && ids.has(par.n.id))) continue; + for (const id of [n.n.id, par.n.id]) if (!ids.has(id)) { ids.add(id); more = true; } + } + } + for (const id of ids) if (!nodes.has(id)) ids.delete(id); + const rim = new Set(); + for (const scene of [a,b]) { + // Use original slots too: some associations have no routed display edge. + for (const n of scene.nodes) for (const e of n.n.edges) if (e.def) { + if (ids.has(n.n.id) && !ids.has(e.def)) rim.add(e.def); + if (ids.has(e.def) && !ids.has(n.n.id)) rim.add(n.n.id); + } + if (scene.edges.some(e => e.role === "PARSER" && ids.has(e.def))) rim.add(0); + } + const parent = new Map([...ids].map(id => [id, id])); + const root = id => { + while (parent.get(id) !== id) id = parent.get(id); + return id; + }; + for (const scene of [a, b]) for (const n of scene.nodes) { + const par = n.par || n.reg; + if (ids.has(n.n.id) && par && ids.has(par.n.id)) parent.set(root(n.n.id), root(par.n.id)); + } + const parts = new Map(), sizes = new Map(); + for (const scene of [a, b]) for (const n of scene.nodes) if (ids.has(n.n.id)) { + const old = sizes.get(n.n.id); + sizes.set(n.n.id, {width: Math.max(n.width, old?.width || 0), height: Math.max(n.height, old?.height || 0)}); + } + for (const id of ids) { + const par = root(id); + if (!parts.has(par)) parts.set(par, []); + parts.get(par).push(nodes.get(id)); + } + const pos = new Map(), children = []; + for (const [id, ns] of parts) { + const n = nodes.get(id), size = sizes.get(id); + const projs = ns.filter(p => p.n.id !== id && p.par).sort((a,b) => a.n.proj.idx-b.n.proj.idx); + const row = projs.reduce((sum,p) => sum + sizes.get(p.n.id).width, 0); + const width = Math.max(size.width, row); + pos.set(id, {x: 0, y: 0, width, height: size.height}); + let x = 0, height = size.height; + for (const p of projs) { + const s = sizes.get(p.n.id), w = s.width + (width-row)/projs.length; + pos.set(p.n.id, {x, y: size.height, width: w, height: s.height}); + x += w; height = Math.max(height, size.height+s.height); + } + x = width; + for (const p of ns.filter(p => p !== n && !projs.includes(p))) { + const s = sizes.get(p.n.id); + pos.set(p.n.id, {x: x+32, y: 0, ...s}); + x += 32+s.width; height = Math.max(height, s.height); + } + children.push({id: String(id), width: x, height}); + } + const edges = new Map(); + for (const scene of [a, b]) for (const e of scene.edges) { + if (!ids.has(e.def) || !ids.has(e.use) || ["ASSOC", "PARSER"].includes(e.role)) continue; + const def = root(e.def), use = root(e.use), id = `${def}:${use}`; + if (def !== use) edges.set(id, {id, sources: [String(def)], targets: [String(use)]}); + } + const graph = await layout.elk.layout({id: "peep", children, edges: [...edges.values()], layoutOptions: { + "elk.algorithm": "layered", "elk.direction": "DOWN", "elk.spacing.nodeNode": 32, + "elk.layered.spacing.nodeNodeBetweenLayers": 48, "elk.padding": "[top=32,left=32,bottom=32,right=32]", + "elk.randomSeed": 1 + }}); + // Gather near the peep's original location, then pan/zoom to the enclosure. + const anchor = a.nodes.find(n => n.n.id === evt.node) || a.nodes.find(n => ids.has(n.n.id)); + const x = (anchor?.x || 0) - graph.width/2, y = (anchor?.y || 0) - graph.height/2; + for (const box of graph.children) for (const n of parts.get(Number(box.id))) { + const p = pos.get(n.n.id); + p.x += box.x+x; p.y += box.y+y; + } + const area = {x, y, width: graph.width, height: graph.height}; + const oldNodes = new Map(a.nodes.map(n => [n.n.id,n])); + const focus = scene => { + const ns = scene.nodes.map(n => { + const p = {...(pos.get(n.n.id) || oldNodes.get(n.n.id) || n)}; + // Clear the enclosure of unrelated boxes. Their exact placement is + // deliberately unimportant here, but they must not obscure the edit. + if (!ids.has(n.n.id) && p.x+p.width > x-24 && p.x < x+area.width+24 && + p.y+p.height > y-24 && p.y < y+area.height+24) { + const dx = (p.x+p.width/2-x-area.width/2)/area.width; + const dy = (p.y+p.height/2-y-area.height/2)/area.height; + if (Math.abs(dx) > Math.abs(dy)) p.x = dx < 0 ? x-p.width-48 : x+area.width+48; + else p.y = dy < 0 ? y-p.height-48 : y+area.height+48; + } + return {...n, x: p.x, y: p.y, width: p.width, height: p.height, + ports: n.ports.map(q => ({...q, x: (q.x+3)*p.width/n.width-3, y: (q.y+3)*p.height/n.height-3}))}; + }); + const byId = new Map(ns.map(n => [n.n.id,n])); + const parser = scene.parser && {...scene.parser, + x: a.parser?.x ?? scene.parser.x, y: a.parser?.y ?? scene.parser.y}; + const es = scene.edges.map(e => { + const def = byId.get(e.def), use = e.role === "PARSER" ? parser : byId.get(e.use); + const p = def?.ports.find(p => p.id === e.sources[0]), q = use?.ports.find(p => p.id === e.targets[0]); + if (!p || !q) return e; + const x = def.x+p.x+3, y = def.y+p.y+3, u = use.x+q.x+3, v = use.y+q.y+3; + const bend = Math.max(24, Math.abs(v-y)/2); + return {...e, path: `M${x},${y}C${x},${y+bend} ${u},${v-bend} ${u},${v}`}; + }); + // Container furniture fades away while the local enclosure explains scope. + return {...scene, nodes: ns, edges: es, parser, groups: a.groups, jumps: [], + peep: {area, ids, rim, changed: diff.changed}}; + }; + return {scenes: [focus(a), focus(b)], area}; + } +} diff --git a/graph/web/render.js b/graph/web/render.js index 7fe058b3..e8b664d2 100644 --- a/graph/web/render.js +++ b/graph/web/render.js @@ -17,6 +17,7 @@ class GraphView { } this.world = this.svg.append("g"); this.regions = this.world.append("g").attr("class", "regions"); + this.halo = this.world.append("rect").attr("class", "peep-area").attr("rx", 36).style("opacity", 0); this.links = this.world.append("g").attr("class", "edges"); this.hits = this.world.append("g").attr("class", "edge-hits"); this.parser = this.world.append("g").attr("class", "parser"); @@ -24,13 +25,14 @@ class GraphView { // Titles/buttons stay above crossing edges, even with narrow group margins. this.heads = this.world.append("g").attr("class", "region-heads"); this.shortcuts = this.world.append("g").attr("class", "shortcuts"); + this.ghosts = this.world.append("g").attr("class", "peep-ghosts").attr("pointer-events", "none"); // Keep the same edge clickable under a stationary mouse during the pan, // even while other nodes/edges pass underneath it. this.hop = null; this.spot = this.svg.append("circle").attr("class", "edge-jump").attr("r", 7).style("display", "none") .on("click", event => { if (this.hop) this.edgeJump(event, this.hop.e, this.hop.path); }); this.zoom = d3.zoom().scaleExtent([.02, 4]).on("zoom", event => { - if (event.sourceEvent) { this.auto = false; this.clearJump(); } + if (event.sourceEvent) { this.auto = false; this.clearJump(); this.svg.interrupt("peep-view"); } this.world.attr("transform", event.transform); }); this.svg.call(this.zoom).on("dblclick.zoom", null) @@ -48,6 +50,7 @@ class GraphView { } show(snap, scene, evt) { + this.cancelMove(); this.clearJump(); if (this.comp !== snap.comp) { this.comp = snap.comp; @@ -64,7 +67,7 @@ class GraphView { return g; }).attr("transform", g => `translate(${g.x},${g.y})`); regions.select("rect.region-box").attr("width", g => g.width).attr("height", g => g.height) - .attr("fill", g => g.n.kind === "LOOP" ? "#f1f6fc" : g.n.kind === "FUN" ? "#fff8ec" : "#f2f7ef"); + .attr("fill", g => g.n.folding ? "#fff0f3" : g.n.kind === "LOOP" ? "#f1f6fc" : g.n.kind === "FUN" ? "#fff8ec" : "#f2f7ef"); const heads = this.heads.selectAll("g.region-head").data(scene.groups || [], g => g.gid).join(enter => { const g = enter.append("g").attr("class", "region-head"); g.append("text").attr("class", "label").attr("x", 26).attr("y", 17); @@ -72,7 +75,7 @@ class GraphView { return g; }).attr("transform", g => `translate(${g.x},${g.y})`); heads.select("text.label").each((g, i, items) => this.label(d3.select(items[i]), - g.n.label.slice(0, Math.max(12, Math.floor((g.width-80)/7))) + ` #${g.gid}`)); + g.n.label.slice(0, Math.max(12, Math.floor((g.width-80-(g.n.folding ? 60 : 0))/7))) + ` #${g.gid}`, g.n.folding)); heads.select("title").text(g => `${g.n.label} · ${g.members.size} nodes`); heads.each((g, i, items) => this.foldButton(d3.select(items[i]), g.gid, false, 4, 4)); this.links.selectAll("path.edge").data(scene.edges, e => e.id).join("path") @@ -116,6 +119,7 @@ class GraphView { boxes.attr("id", n => n.id).attr("data-id", n => n.n.id) .classed("held", n => !!n.held) .classed("folded", n => !!n.n.fold) + .classed("folding", n => !!n.n.folding) .attr("transform", n => `translate(${n.x},${n.y})`) .on("click", (event, n) => { event.stopPropagation(); this.clearJump(); this.select(n.n.id); }); boxes.select("path.box").attr("d", n => this.shape(n)) @@ -123,7 +127,7 @@ class GraphView { UNIT: "#ffe0ab", START: "#fff1c4", STOP: "#fff1c4", MEM: "#dcecff", PHI: "#f5e4fa", SCOPE: "#e5e0ff", DATA: "#edf3f7"})[n.n.kind]); boxes.select("text.label").attr("x", n => (n.width - (n.n.fold ? 24 : 0)) / 2) .attr("y", n => n.compact ? 17 : n.scope ? 48 : 18) - .each((n, i, items) => this.label(d3.select(items[i]), n.label)); + .each((n, i, items) => this.label(d3.select(items[i]), n.label, n.n.folding)); boxes.select("text.type").attr("x", n => n.width / 2).text(n => n.scope || n.compact ? "" : n.type); boxes.select("title").text(n => this.info(n.n)); boxes.each((n, i, groups) => { @@ -174,15 +178,122 @@ class GraphView { jumps.select("title").text(j => `Center function #${j.target}\n` + j.refs.map(r => `#${r.use}[${r.idx}] → #${r.def} · ${r.role}`).join("\n")); this.assocs(document.getElementById("assocs").checked); + const peep = scene.peep; + if (peep) this.halo.attr("x", peep.area.x).attr("y", peep.area.y) + .attr("width", peep.area.width).attr("height", peep.area.height); + this.halo.style("opacity", peep ? 1 : 0); + this.nodes.selectAll("g.node").style("opacity", n => !peep || peep.ids.has(n.n.id) ? 1 : peep.rim.has(n.n.id) ? .4 : .08) + .classed("peep-edit", n => !!peep?.changed.has(n.n.id)); + this.links.selectAll("path.edge").style("opacity", e => !peep ? 1 : + peep.ids.has(e.def) && peep.ids.has(e.use) ? 1 : peep.ids.has(e.def) || peep.ids.has(e.use) ? .4 : .04); + this.hits.style("pointer-events", peep ? "none" : null).style("display", peep ? "none" : null); + for (const layer of [this.regions, this.heads, this.parser, this.shortcuts]) + layer.style("opacity", peep ? 0 : 1).style("pointer-events", peep ? "none" : null); + if (peep?.rim.has(0)) this.parser.style("opacity", .4); + this.nodes.selectAll(".fold-btn").style("display", peep ? "none" : null); this.mark(); this.select(this.pick); if (this.auto) this.fit(); } - label(host, text) { + cancelMove() { + this.svg.interrupt("peep-view"); + this.world.selectAll("*").interrupt("peep-move"); + this.ghosts.selectAll("*").remove(); + } + + // Sample paths so an orthogonal route can smoothly become a local curve. + // Rewired edges cross-fade instead of appearing to sweep through other defs. + pathTween(path, from, to) { + const points = d => { + path.setAttribute("d", d); + const len = path.getTotalLength(); + return Array.from({length: 33}, (_,i) => { + const p = path.getPointAtLength(len*i/32); + return [p.x,p.y]; + }); + }; + const a = points(from), b = points(to), mix = d3.interpolateArray(a,b); + path.setAttribute("d", from); + return t => t === 1 ? to : mix(t).map((p,i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join(" "); + } + + async move(snap, scene, evt, view) { + this.cancelMove(); + this.clearJump(); + const ms = matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 480; + const now = d3.zoomTransform(this.svg.node()); + const oldNodes = new Map(), oldEdges = new Map(); + this.nodes.selectAll("g.node").each(function(n) { + oldNodes.set(n.id, {el: this.cloneNode(true), at: this.getAttribute("transform"), opacity: this.style.opacity}); + }); + this.links.selectAll("path.edge").each(function(e) { + oldEdges.set(e.id, {e, el: this.cloneNode(true), path: this.getAttribute("d"), opacity: this.style.opacity}); + }); + const opacity = new Map([this.regions, this.heads, this.parser, this.shortcuts, this.halo].map(s => [s, s.style("opacity")])); + this.auto = false; + this.show(snap, scene, evt); + const jobs = []; + const tween = s => { + const t = s.transition("peep-move").duration(ms).ease(d3.easeCubicInOut); + jobs.push(t.end().catch(() => {})); + return t; + }; + const ghost = (el, opacity) => { + el.removeAttribute("id"); + el.querySelectorAll("[id]").forEach(n => n.removeAttribute("id")); + this.ghosts.node().append(el); + tween(d3.select(el).style("opacity", opacity || 1)).style("opacity", 0).remove(); + }; + this.nodes.selectAll("g.node").each((n,i,items) => { + const s = d3.select(items[i]), at = s.attr("transform"), opacity = s.style("opacity"); + const old = oldNodes.get(n.id); + oldNodes.delete(n.id); + s.attr("transform", old?.at || at).style("opacity", old?.opacity || 0); + tween(s).attr("transform", at).style("opacity", opacity); + }); + for (const n of oldNodes.values()) ghost(n.el, n.opacity); + this.links.selectAll("path.edge").each((e,i,items) => { + const s = d3.select(items[i]), opacity = s.style("opacity"), old = oldEdges.get(e.id); + oldEdges.delete(e.id); + if (old && old.e.def === e.def && old.e.use === e.use) { + const path = this.pathTween(items[i], old.path, e.path); + tween(s.style("opacity", old.opacity)).attrTween("d", () => path).style("opacity", opacity); + } else { + if (old) ghost(old.el, old.opacity); + tween(s.style("opacity", 0)).style("opacity", opacity); + } + }); + for (const e of oldEdges.values()) ghost(e.el, e.opacity); + for (const [s, old] of opacity) { + const next = s.style("opacity"); + tween(s.style("opacity", old)).style("opacity", next); + } + // Hide hit targets until the drawn routes reach their destination. + this.hits.style("display", "none"); + const mix = d3.interpolateArray([now.x,now.y,now.k], [view.x,view.y,view.k]); + jobs.push(this.svg.transition("peep-view").duration(ms).ease(d3.easeCubicInOut) + .tween("view", () => t => { + const [x,y,k] = mix(t); + this.svg.call(this.zoom.transform, d3.zoomIdentity.translate(x,y).scale(k)); + }).end().catch(() => {})); + await Promise.all(jobs); + if (this.scene === scene) this.hits.style("display", scene.peep ? "none" : null); + } + + view(area = this.scene) { + const w = this.host.clientWidth, h = this.host.clientHeight; + const k = Math.max(.02, Math.min(1.5, (w-32)/area.width, (h-32)/area.height)); + return d3.zoomIdentity.translate((w-area.width*k)/2-(area.x || 0)*k, + (h-area.height*k)/2-(area.y || 0)*k).scale(k); + } + + label(host, text, folding = false) { const split = text.lastIndexOf(" #"); - host.selectAll("tspan").data([text.slice(0, split), "\u00a0\u00a0" + text.slice(split + 1)]).join("tspan") - .attr("class", (_, i) => i ? "node-id" : null).text(s => s); + const parts = [text.slice(0, split), "\u00a0\u00a0" + text.slice(split + 1)]; + if (folding) parts.push("\u00a0\u00a0FOLDING"); + host.selectAll("tspan").data(parts).join("tspan") + .attr("class", (_, i) => i === 2 ? "fold-tag" : i ? "node-id" : null).text(s => s); } foldButton(host, id, folded, x, y) { @@ -221,6 +332,7 @@ class GraphView { const fold = this.scene.nodes.find(v => v.n.id === n.id)?.n.fold; n = this.scene.raw?.get(n.id) || n; return `#${n.id} ${n.label}${n.proj ? "\nProjection " + n.proj.idx + " of #" + n.proj.par : ""}` + + (n.folding ? "\nFOLDING: being inlined; pending removal" : "") + (fold ? `\n${fold.members.size} nodes folded` : "") + (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") : "") + @@ -240,6 +352,7 @@ class GraphView { detail.hidden = !id; const box = this.scene?.cover?.get(id); detail.textContent = node ? this.info(node) + (box && box !== id ? `\nInside folded region #${box}` : "") : `#${id} is absent in this frame.`; + if (this.evt?.kind === "ERROR" && this.evt.node === id) detail.textContent += "\n\n" + this.evt.msg; } assocs(show) { @@ -281,21 +394,23 @@ class GraphView { d3.zoomIdentity.translate(ix(t),iy(t)).scale(now.k))); } - center(id) { + center(id, readable = false) { this.clearJump(); const dst = this.scene.cover?.get(id) || id; const n = this.scene.nodes.find(n => n.n.id === dst); if (!n) return; this.auto = false; this.select(id); + if (readable) this.svg.call(this.zoom.scaleTo, Math.min(1.2, (this.host.clientWidth - 40) / n.width)); this.svg.call(this.zoom.translateTo, n.x + n.width / 2, n.y + n.height / 2); } mark() { const evt = this.evt; const show = document.getElementById("near").checked; - const near = new Set(show ? evt?.near : []); - this.nodes.selectAll("g.node").classed("near", n => near.has(n.n.id) || + const near = new Set(show ? this.scene?.peep?.ids || evt?.near : []); + this.nodes.selectAll("g.node").classed("error", n => evt?.kind === "ERROR" && n.n.id === evt.node) + .classed("near", n => near.has(n.n.id) || n.n.fold && [...n.n.fold.members].some(id => near.has(id))) .classed("focus", n => show && (n.n.id === evt?.node || n.n.id === evt?.repl || n.n.fold && (n.n.fold.members.has(evt?.node) || n.n.fold.members.has(evt?.repl)))); @@ -332,9 +447,6 @@ class GraphView { fit() { this.clearJump(); if (!this.scene || !this.host.clientWidth || !this.host.clientHeight) return; - const w = this.host.clientWidth, h = this.host.clientHeight; - const scale = Math.max(.02, Math.min(1.5, (w - 32) / this.scene.width, (h - 32) / this.scene.height)); - this.svg.call(this.zoom.transform, d3.zoomIdentity - .translate((w - this.scene.width * scale) / 2, (h - this.scene.height * scale) / 2).scale(scale)); + this.svg.call(this.zoom.transform, this.view()); } } diff --git a/graph/web/viewer.js b/graph/web/viewer.js index 7e41c082..6309deae 100644 --- a/graph/web/viewer.js +++ b/graph/web/viewer.js @@ -19,6 +19,53 @@ let compiling = false; let connection = "Connecting to compiler..."; let failure = ""; let busy = ""; +let peep = null; +let stepping = false; +let motion = 0; + +// Nested captures describe work inside an enclosing edit, not stable stops. +// Keep capture numbers, but make navigation land on the outer before/result. +function stable(index) { + while (index > 0 && frames[index]?.evt?.up) index--; + const end = resultEnd(index); + if (end < 0 || end > index) { + const id = frames[index].evt.peep; + while (index > 0 && !(frames[index].evt.kind === "BEFORE" && frames[index].evt.peep === id)) index--; + } + return index; +} + +function resultEnd(index) { + const frame = frames[index], evt = frame?.evt; + if (evt?.kind !== "RETURN" || evt.up || !evt.repl || evt.repl === evt.node) return index; + if (frame.attach !== undefined) return frame.attach; + if (!frame.snap.nodes.some(n => n.id === evt.node)) return frame.attach = index; + const next = frames[index+1]; + if (!next) return done || failure ? index : -1; + return frame.attach = !next.evt.up && ["BEFORE", "PHASE"].includes(next.evt.kind) && + next.evt.phase === evt.phase && GraphPeep.attached(frame.snap, next.snap, evt) ? index+1 : index; +} + +function editEvent(from, to) { + const evt = frames[to-1]?.evt; + return evt?.peep === frames[from].evt?.peep && resultEnd(to-1) === to ? evt : frames[to].evt; +} + +function nextFrame(index, dir) { + const evt = frames[index]?.evt; + const pair = dir > 0 ? evt?.kind === "BEFORE" : ["RETURN", "APPLY"].includes(evt?.kind); + if (pair) { + for (let i = index+dir; i >= 0 && i < frames.length; i += dir) { + const e = frames[i].evt; + if (e?.peep === evt.peep && (dir > 0 ? ["RETURN", "APPLY"].includes(e.kind) : e.kind === "BEFORE")) + return dir > 0 ? resultEnd(i) : i; + } + // A streaming capture hasn't delivered this peep's result yet. + return -1; + } + const dest = index+dir; + return dest >= 0 && dest < frames.length ? stable(dest) : -1; +} function updateUI() { status.textContent = failure || (connection !== "Connected" ? connection : @@ -32,31 +79,36 @@ function updateUI() { scrub.value = String(Math.max(1, at + 1)); scrub.setAttribute("aria-valuetext", `Frame ${at + 1} of ${frames.length}`); document.getElementById("len").textContent = String(frames.length); - document.getElementById("doFirst").disabled = document.getElementById("doPrev").disabled = - !rendererReady || at <= 0; - document.getElementById("doLast").disabled = document.getElementById("doNext").disabled = - !rendererReady || !frames.length || at + 1 >= frames.length; + document.getElementById("doFirst").disabled = !rendererReady || !frames.length || (!peep && !stepping && at <= 0); + document.getElementById("doLast").disabled = !rendererReady || !frames.length || (!peep && !stepping && at >= stable(frames.length-1)); + document.getElementById("doPrev").disabled = !rendererReady || rendering || stepping || (!peep && at <= 0); + document.getElementById("doNext").disabled = !rendererReady || rendering || stepping || (!peep && nextFrame(at,1) < 0); document.getElementById("compile").disabled = - !socket || socket.readyState !== WebSocket.OPEN || compiling || rendering; + !socket || socket.readyState !== WebSocket.OPEN || compiling || rendering || stepping; document.getElementById("fit").disabled = rendering || current < 0; - document.getElementById("save").disabled = rendering || !frames[current]?.snap; - document.getElementById("unfold").disabled = !folded.size; + document.getElementById("save").disabled = rendering || stepping || !!peep || !frames[current]?.snap; + document.getElementById("unfold").disabled = !folded.size || !!peep || stepping; document.getElementById("assocs").disabled = !frames[current]?.snap; document.getElementById("near").disabled = !frames[current]?.evt; - const evt = frames[current]?.evt; + const result = peep?.evt; + const evt = peep ? (peep.stage === 1 ? {...result, kind: "BEFORE"} : result) : frames[current]?.evt; let label = ""; if (evt) { - const text = evt.kind === "PHASE" ? "Phase complete" : + const text = evt.kind === "ERROR" ? `Error${evt.node ? " at node #" + evt.node : ""}: ${evt.msg}` : + evt.kind === "PHASE" ? "Phase complete" : evt.kind === "BEFORE" ? `Before peephole #${evt.peep} at node #${evt.node}` : evt.kind === "RETURN" ? `Return #${evt.repl} for #${evt.node}` : !evt.repl ? `Removed #${evt.node}` : evt.repl === evt.node ? `Updated #${evt.node}` : `Replaced #${evt.node} with #${evt.repl}`; label = `${evt.phase} · ${text}${evt.up ? " (inside peephole #" + evt.up + ")" : ""}`; } + if (peep) label += ` · ${peep.stage === 1 ? "Gathered — next: rewrite" : "Rewritten — next: release"} (between frames ${peep.from+1} and ${peep.to+1})`; document.getElementById("event").textContent = label; + document.getElementById("event").classList.toggle("error", evt?.kind === "ERROR"); } function reportError(error) { + cancelPeep(); failure = "Viewer error: " + (error && error.message || error || "Graph rendering failed"); rendering = false; wanted = -1; @@ -76,6 +128,7 @@ function get_program() { return; } if (compiling || rendering) return; + cancelPeep(); generation++; frames = []; current = -1; @@ -91,6 +144,8 @@ function get_program() { async function render(index) { if (!rendererReady || index < 0 || index >= frames.length) return; + index = stable(index); + cancelPeep(); wanted = index; if (rendering) { updateUI(); return; } const frameGeneration = generation; @@ -101,30 +156,16 @@ async function render(index) { index = wanted; const frame = frames[index]; const key = foldKey(); - let scene = key ? (frame.foldKey === key ? frame.foldLayout : null) : frame.layout; - busy = !scene ? "Laying out frame " + (index + 1) + "..." : "Drawing..."; + busy = "Laying out frame " + (index + 1) + "..."; updateUI(); - if (!scene) { - scene = await layout.run(frame.snap, frame.evt, new Set(folded)); - // Keep the ordinary layout plus only the most recent folded variant. - if (key) { frame.foldKey = key; frame.foldLayout = scene; } - else frame.layout = scene; - } + const scene = await sceneFor(frame); if (generation !== frameGeneration) break; // Scrubbing can change the destination while ELK works. Draw only the // latest request, without computing all the intervening frames. if (wanted !== index || foldKey() !== key) continue; renderer.show(frame.snap, scene, frame.evt); - if (frame.pos >= 0) { - // A newline or EOF has no visible character to highlight. Show the last - // parsed character there, rather than losing the position indicator. - let pos = Math.min(frame.pos, program.value.length); - while (pos > 0 && (!program.value[pos] || /\s/.test(program.value[pos]))) pos--; - program.setSelectionRange(pos, pos + 1); - // Focus after selecting, so the browser brings the new position into view. - if (document.activeElement !== frameNo && document.activeElement !== scrub) - program.focus({preventScroll: true}); - } + if (frame.evt.kind === "ERROR" && frame.evt.node) renderer.center(frame.evt.node, true); + cursor(frame); current = index; wanted = -1; } @@ -136,17 +177,125 @@ async function render(index) { } } +async function sceneFor(frame, folds = folded) { + if (!layout) layout = new GraphLayout(); + // Reveal the diagnostic even inside a folded function/loop. Keep the user's + // fold choices for earlier frames. + if (frame.evt.kind === "ERROR" && frame.evt.node && folds.size) { + folds = new Set(folds); + const view = GraphGroups.view(frame.snap, new Set()); + for (const id of folds) + if (view.groups.get(id)?.members.has(frame.evt.node)) folds.delete(id); + } + const key = [...folds].sort((a,b) => a-b).join(","); + let scene = key ? (frame.foldKey === key ? frame.foldLayout : null) : frame.layout; + if (!scene) { + scene = await layout.run(frame.snap, frame.evt, new Set(folds)); + // Ordinary layout plus only the most recent folded variant. + if (key) { frame.foldKey = key; frame.foldLayout = scene; } + else frame.layout = scene; + } + return scene; +} + +function cursor(frame) { + if (frame.pos < 0) return; + // Keep the last visible parsed character selected at whitespace/EOF. + let pos = Math.min(frame.pos, program.value.length); + while (pos > 0 && (!program.value[pos] || /\s/.test(program.value[pos]))) pos--; + program.setSelectionRange(pos, pos + 1); + // Keep navigation focus: moving the source cursor must not turn the next + // playback arrow into a text-editing key. +} + +function cancelPeep() { + motion++; + stepping = false; + renderer?.cancelMove(); + if (peep) { + renderer.auto = peep.auto; + renderer.svg.call(renderer.zoom.transform, peep.view); + peep = null; + } +} + +function releaseView(p, scene) { + if (p.auto) return renderer.view(scene); + // Keep a nearby surviving node at the same screen position after relayout. + const [x,y] = p.view.invert([renderer.host.clientWidth/2, renderer.host.clientHeight/2]); + const nodes = new Map(scene.nodes.map(n => [n.id,n])); + let anchor, dist = Infinity; + for (const n of p.entry.nodes) if (nodes.has(n.id)) { + const d = Math.hypot(n.x+n.width/2-x, n.y+n.height/2-y); + if (d < dist) { anchor = n; dist = d; } + } + if (!anchor) return p.view; + const n = nodes.get(anchor.id); + return p.view.translate(anchor.x+anchor.width/2-n.x-n.width/2, + anchor.y+anchor.height/2-n.y-n.height/2); +} + +async function step(dir) { + if (!rendererReady || rendering || stepping) return; + const ticket = ++motion; + try { + if (!peep) { + const dest = nextFrame(current,dir); + if (current < 0 || dest < 0 || dest >= frames.length) return; + const from = Math.min(current,dest), to = Math.max(current,dest); + const evt = editEvent(from,to); + // Pair the enclosing attempt's before/result, including any recursive + // construction of replacement nodes. Never expose those nodes early. + const diff = ["RETURN", "APPLY"].includes(evt?.kind) && GraphPeep.diff(frames[from].snap, frames[to].snap, evt); + if (!diff?.structural) return render(dest); + stepping = true; + busy = "Gathering peephole neighborhood..."; + const view = d3.zoomTransform(renderer.svg.node()), auto = renderer.auto, entry = renderer.scene; + updateUI(); + const local = await GraphPeep.make(frames[from], frames[to], diff, layout, folded, sceneFor, evt); + if (ticket !== motion) return; + peep = {...local, from, to, evt, stage: dir > 0 ? 0 : 3, view, auto, entry}; + } + stepping = true; + const p = peep, stage = p.stage+dir; + const index = stage <= 1 ? p.from : p.to, frame = frames[index]; + busy = stage === 1 ? "Gathering..." : stage === 2 ? "Rewriting..." : "Returning to graph..."; + updateUI(); + const scene = stage === 0 || stage === 3 ? await sceneFor(frame) : p.scenes[stage-1]; + if (ticket !== motion) return; + const view = scene.peep ? renderer.view(p.area) : releaseView(p, scene); + p.stage = stage; + current = index; + cursor(frame); + updateUI(); + await renderer.move(frame.snap, scene, p.evt, view); + if (ticket !== motion) return; + if (!scene.peep) { + renderer.auto = p.auto; + renderer.evt = frame.evt; + renderer.mark(); + peep = null; + } + stepping = false; + busy = ""; + updateUI(); + } catch (error) { + if (ticket === motion) reportError(error); + } +} + function doFirst() { render(0); } function toggleFold(id) { + if (peep || stepping) return; if (folded.has(id)) folded.delete(id); else folded.add(id); render(wanted >= 0 ? wanted : current); } function doLast() { render(frames.length - 1); } -function doNext() { render((wanted >= 0 ? wanted : current) + 1); } -function doPrev() { render((wanted >= 0 ? wanted : current) - 1); } +function doNext() { return step(1); } +function doPrev() { return step(-1); } function jump() { const n = frameNo.valueAsNumber; - const index = Number.isFinite(n) ? Math.max(0, Math.min(frames.length - 1, Math.trunc(n) - 1)) : current; + const index = stable(Number.isFinite(n) ? Math.max(0, Math.min(frames.length - 1, Math.trunc(n) - 1)) : current); frameNo.value = String(index + 1); render(index); } @@ -170,7 +319,21 @@ frameNo.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); jump(); } }); scrub.addEventListener("input", () => render(scrub.valueAsNumber - 1)); +scrub.addEventListener("keydown", event => { + const dir = {ArrowLeft: -1, ArrowDown: -1, ArrowRight: 1, ArrowUp: 1}[event.key]; + if (!dir) return; + event.preventDefault(); + // A native one-frame increment could land inside the same enclosing peep + // and snap back forever. Slider keys skip directly to the next stable stop. + const index = nextFrame(wanted >= 0 ? wanted : current, dir); + if (index >= 0) render(index); +}); document.getElementById("fit").addEventListener("click", () => { + if (peep) { + renderer.svg.interrupt("peep-view"); + renderer.svg.call(renderer.zoom.transform, renderer.view(peep.area)); + return; + } renderer.auto = true; renderer.fit(); }); @@ -178,7 +341,16 @@ document.getElementById("assocs").addEventListener("change", event => renderer.a document.getElementById("near").addEventListener("change", () => renderer.mark()); document.getElementById("save").addEventListener("click", () => renderer.save(frames[current].snap.step)); document.addEventListener("keydown", event => { - if (event.key === "Escape") { renderer.clearJump(); renderer.select(0); } + if (!event.defaultPrevented && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey && + !event.target.closest("input, textarea, select") && !event.target.isContentEditable && + (event.key === "ArrowLeft" || event.key === "ArrowRight")) { + event.preventDefault(); + document.getElementById(event.key === "ArrowLeft" ? "doPrev" : "doNext").click(); + } + if (event.key === "Escape") { + if (peep || stepping) render(current); + renderer.clearJump(); renderer.select(0); + } }); try { -- cgit v1.2.3