Skip to content

Commit 845dcfe

Browse files
committed
Hide empty and disconnected subgraphs
1 parent 4559d4b commit 845dcfe

5 files changed

Lines changed: 279 additions & 129 deletions

File tree

src/main/java/nextflow/lsp/services/script/dag/DataflowVisitor.java

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.codehaus.groovy.ast.expr.VariableExpression;
4747
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
4848
import org.codehaus.groovy.ast.stmt.IfStatement;
49+
import org.codehaus.groovy.ast.stmt.Statement;
4950
import org.codehaus.groovy.control.SourceUnit;
5051
import org.codehaus.groovy.syntax.Types;
5152

@@ -108,10 +109,8 @@ public void visitWorkflow(WorkflowNode node) {
108109
var name = node.isEntry() ? "<entry>" : node.getName();
109110
visitWorkflowTakes(node, current.inputs);
110111
visit(node.main);
111-
if( node.isEntry() )
112-
visitWorkflowPublishers(node, current.outputs);
113-
else
114-
visitWorkflowEmits(node, current.outputs);
112+
var outputs = node.isEntry() ? node.publishers : node.emits;
113+
visitWorkflowOutputs(outputs, current.outputs);
115114
graphs.put(name, current);
116115
inEntry = false;
117116
current = null;
@@ -121,13 +120,14 @@ private void visitWorkflowTakes(WorkflowNode node, Map<String,Node> result) {
121120
for( var stmt : asBlockStatements(node.takes) ) {
122121
var name = asVarX(stmt).getName();
123122
var dn = addNode(name, Node.Type.NAME, stmt);
123+
dn.verbose = false;
124124
vc.putSymbol(name, dn);
125125
result.put(name, dn);
126126
}
127127
}
128128

129-
private void visitWorkflowEmits(WorkflowNode node, Map<String,Node> result) {
130-
for( var stmt : asBlockStatements(node.emits) ) {
129+
private void visitWorkflowOutputs(Statement outputs, Map<String,Node> result) {
130+
for( var stmt : asBlockStatements(outputs) ) {
131131
var emit = ((ExpressionStatement) stmt).getExpression();
132132
String name;
133133
if( emit instanceof VariableExpression ve ) {
@@ -143,23 +143,10 @@ else if( emit instanceof AssignmentExpression assign ) {
143143
visit(new AssignmentExpression(varX(name), emit));
144144
}
145145
var dn = getSymbol(name);
146-
if( dn == null )
147-
System.err.println("missing emit: " + name);
148-
result.put(name, dn);
149-
}
150-
}
151-
152-
private void visitWorkflowPublishers(WorkflowNode node, Map<String,Node> result) {
153-
for( var stmt : asBlockStatements(node.publishers) ) {
154-
var es = (ExpressionStatement) stmt;
155-
var publisher = (BinaryExpression) es.getExpression();
156-
var target = asVarX(publisher.getLeftExpression());
157-
var source = publisher.getRightExpression();
158-
visit(new AssignmentExpression(target, source));
159-
var name = target.getName();
160-
var dn = getSymbol(name);
161-
if( dn == null )
162-
System.err.println("missing publisher: " + name);
146+
if( dn != null )
147+
dn.verbose = false;
148+
else
149+
System.err.println("missing output: " + name);
163150
result.put(name, dn);
164151
}
165152
}
@@ -176,27 +163,45 @@ public void visitIfElse(IfStatement node) {
176163
vc.pushScope();
177164
current.pushSubgraph(controlDn);
178165
visitWithPreds(node.getIfBlock());
179-
current.popSubgraph();
166+
var ifSubgraph = current.popSubgraph();
180167
var ifScope = vc.popScope();
181168

182169
// visit the else branch
170+
Subgraph elseSubgraph;
183171
Map<String,Variable> elseScope;
184172

185173
if( !node.getElseBlock().isEmpty() ) {
186174
vc.pushScope();
187175
current.pushSubgraph(controlDn);
188176
visitWithPreds(node.getElseBlock());
189-
current.popSubgraph();
177+
elseSubgraph = current.popSubgraph();
190178
elseScope = vc.popScope();
191179
}
192180
else {
193181
// if there is no else branch, then the set of active symbols
194182
// after the if statement is the union of the active symbols
195183
// from before the if and the active symbols in the if
184+
elseSubgraph = null;
196185
elseScope = vc.peekScope();
197186
}
198187

199-
vc.mergeConditionalScopes(ifScope, elseScope);
188+
// apply variables from if and else scopes to current scope
189+
var outputs = vc.mergeConditionalScopes(ifScope, elseScope);
190+
191+
for( var name : outputs ) {
192+
var preds = vc.getSymbolPreds(name);
193+
if( preds.size() > 1 ) {
194+
var dn = current.addNode(name, Node.Type.NAME, null, preds);
195+
vc.putSymbol(name, dn);
196+
}
197+
}
198+
199+
// hide if-else statement if both subgraphs are empty
200+
if( ifSubgraph.isVerbose() && (elseSubgraph == null || elseSubgraph.isVerbose()) ) {
201+
controlDn.verbose = true;
202+
for( var name : outputs )
203+
getSymbol(name).preds.addAll(controlPreds);
204+
}
200205
}
201206

202207
// expressions
@@ -305,13 +310,19 @@ public void visitTernaryExpression(TernaryExpression node) {
305310

306311
current.pushSubgraph(controlDn);
307312
var truePreds = visitWithPreds(node.getTrueExpression());
308-
current.popSubgraph();
313+
var trueSubgraph = current.popSubgraph();
309314
currentPreds().addAll(truePreds);
310315

311316
current.pushSubgraph(controlDn);
312317
var falsePreds = visitWithPreds(node.getFalseExpression());
313-
current.popSubgraph();
318+
var falseSubgraph = current.popSubgraph();
314319
currentPreds().addAll(falsePreds);
320+
321+
// hide ternary expression if both subgraphs are empty
322+
if( trueSubgraph.isVerbose() && falseSubgraph.isVerbose() ) {
323+
controlDn.verbose = true;
324+
currentPreds().addAll(controlPreds);
325+
}
315326
}
316327

317328
@Override
@@ -326,6 +337,7 @@ public void visitPropertyExpression(PropertyExpression node) {
326337
if( !current.inputs.containsKey(name) )
327338
current.inputs.put(name, addNode(name, Node.Type.NAME, null));
328339
var dn = current.inputs.get(name);
340+
dn.verbose = false;
329341
currentPreds().add(dn);
330342
return;
331343
}
@@ -447,12 +459,12 @@ public void visitVariableExpression(VariableExpression node) {
447459
// helpers
448460

449461
private Node getSymbol(String name) {
450-
var preds = vc.getSymbol(name);
462+
var preds = vc.getSymbolPreds(name);
451463
if( preds.isEmpty() )
452464
return null;
453-
if( preds.size() == 1 )
454-
return preds.iterator().next();
455-
return current.addNode(name, Node.Type.NAME, null, preds);
465+
if( preds.size() > 1 )
466+
System.err.println("unmerged symbol " + name + " " + preds);
467+
return preds.iterator().next();
456468
}
457469

458470
private Set<Node> currentPreds() {

src/main/java/nextflow/lsp/services/script/dag/Graph.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ public Subgraph(int id, Node pred) {
9090
this.id = id;
9191
this.pred = pred;
9292
}
93+
94+
public boolean isVerbose() {
95+
return nodes.stream().allMatch(n -> n.verbose);
96+
}
97+
98+
@Override
99+
public boolean equals(Object other) {
100+
return other instanceof Subgraph s && this.id == s.id;
101+
}
102+
103+
@Override
104+
public int hashCode() {
105+
return id;
106+
}
93107
}
94108

95109

@@ -106,12 +120,15 @@ public enum Type {
106120
public final URI uri;
107121
public final Set<Node> preds;
108122

123+
public boolean verbose;
124+
109125
public Node(int id, String label, Type type, URI uri, Set<Node> preds) {
110126
this.id = id;
111127
this.label = label;
112128
this.type = type;
113129
this.uri = uri;
114130
this.preds = preds;
131+
this.verbose = (type == Type.NAME);
115132
}
116133

117134
public void addPredecessors(Set<Node> preds) {
@@ -130,6 +147,7 @@ public int hashCode() {
130147

131148
@Override
132149
public String toString() {
133-
return String.format("id=%s,label='%s',type=%s", id, label, type);
150+
var predIds = preds.stream().map(p -> p.id).toList();
151+
return String.format("id=%s,label='%s',type=%s,preds=%s", id, label, type, predIds);
134152
}
135153
}

src/main/java/nextflow/lsp/services/script/dag/MermaidRenderer.java

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Collection;
20+
import java.util.HashMap;
2021
import java.util.HashSet;
2122
import java.util.List;
23+
import java.util.Map;
2224
import java.util.Set;
2325
import java.util.stream.Collectors;
2426
import java.util.stream.Stream;
@@ -91,9 +93,17 @@ public String render(String name, Graph graph) {
9193
append("end");
9294
}
9395

94-
// render nodes
96+
// render nodes and subgraphs
97+
var root = graph.peekSubgraph();
98+
99+
root.nodes.stream()
100+
.filter(n -> !inputs.contains(n))
101+
.filter(n -> !outputs.contains(n))
102+
.forEach(this::renderNode);
103+
95104
var allSubgraphs = new ArrayList<Subgraph>();
96-
renderSubgraph(graph.peekSubgraph(), allSubgraphs);
105+
for( var s : root.subgraphs )
106+
renderSubgraph(s, allSubgraphs);
97107

98108
// render outputs
99109
if( outputs.size() > 0 ) {
@@ -106,27 +116,13 @@ public String render(String name, Graph graph) {
106116
}
107117

108118
// render edges
119+
var visited = new HashMap<Node,Set<Node>>();
120+
109121
for( var dn : nodes ) {
110-
if( isHidden(dn, inputs, outputs) )
122+
if( isHidden(dn) )
111123
continue;
112124

113-
var preds = dn.preds;
114-
var visited = new HashSet<Node>();
115-
while( true ) {
116-
var done = preds.stream().allMatch(p -> !isHidden(p, inputs, outputs));
117-
if( done )
118-
break;
119-
visited.addAll(preds);
120-
preds = preds.stream()
121-
.flatMap(pred -> (
122-
isHidden(pred, inputs, outputs)
123-
? pred.preds.stream().filter(p -> !visited.contains(p))
124-
: Stream.of(pred)
125-
))
126-
.collect(Collectors.toSet());
127-
}
128-
129-
for( var dnPred : preds )
125+
for( var dnPred : visiblePreds(dn, visited) )
130126
append("v%d --> v%d", dnPred.id, dn.id);
131127
}
132128

@@ -149,50 +145,40 @@ public String render(String name, Graph graph) {
149145
* @param allSubgraphs
150146
*/
151147
private void renderSubgraph(Subgraph subgraph, List<Subgraph> allSubgraphs) {
152-
allSubgraphs.add(subgraph);
153-
154-
if( subgraph.id > 0 ) {
155-
append("subgraph s%d[\" \"]", subgraph.id);
156-
incIndent();
157-
}
148+
if( isHidden(subgraph) )
149+
return;
158150

159-
// render nodes
160-
for( var dn : subgraph.nodes ) {
161-
if( isHidden(dn) )
162-
continue;
151+
allSubgraphs.add(subgraph);
163152

164-
var label = dn.label
165-
.replaceAll("\n", "\\\\\n")
166-
.replaceAll("\"", "\\\\\"");
153+
append("subgraph s%d[\" \"]", subgraph.id);
154+
incIndent();
167155

168-
append(renderNode(dn.id, label, dn.type));
169-
if( dn.uri != null )
170-
append("click v%d href \"%s\" _blank", dn.id, dn.uri.toString());
171-
}
156+
for( var dn : subgraph.nodes )
157+
renderNode(dn);
172158

173-
// render subgraphs
174159
for( var s : subgraph.subgraphs )
175160
renderSubgraph(s, allSubgraphs);
176161

177-
if( subgraph.id > 0 ) {
178-
decIndent();
179-
append("end");
180-
}
162+
decIndent();
163+
append("end");
181164
}
182165

183166
/**
184-
* Only inputs, outputs, and processes/workflows are currently shown.
167+
* Render a node.
185168
*
186169
* @param dn
187-
* @param inputs
188-
* @param outputs
189170
*/
190-
private boolean isHidden(Node dn, Collection<Node> inputs, Collection<Node> outputs) {
191-
return isHidden(dn) && !inputs.contains(dn) && !outputs.contains(dn);
192-
}
171+
private void renderNode(Node dn) {
172+
if( isHidden(dn) )
173+
return;
193174

194-
private boolean isHidden(Node dn) {
195-
return !verbose && dn.type == Node.Type.NAME;
175+
var label = dn.label
176+
.replaceAll("\n", "\\\\\n")
177+
.replaceAll("\"", "\\\\\"");
178+
179+
append(renderNode(dn.id, label, dn.type));
180+
if( dn.uri != null )
181+
append("click v%d href \"%s\" _blank", dn.id, dn.uri.toString());
196182
}
197183

198184
private static String renderNode(int id, String label, Node.Type type) {
@@ -203,4 +189,53 @@ private static String renderNode(int id, String label, Node.Type type) {
203189
};
204190
}
205191

192+
/**
193+
* Get the set of visible predecessors for a node.
194+
*
195+
* @param dn
196+
* @param visited
197+
*/
198+
private Set<Node> visiblePreds(Node dn, Map<Node,Set<Node>> visited) {
199+
if( visited.containsKey(dn) )
200+
return visited.get(dn);
201+
202+
var result = dn.preds.stream()
203+
.flatMap(pred -> (
204+
isHidden(pred)
205+
? visiblePreds(pred, visited).stream()
206+
: Stream.of(pred)
207+
))
208+
.collect(Collectors.toSet());
209+
visited.put(dn, result);
210+
return result;
211+
}
212+
213+
/**
214+
* When verbose mode is disabled, all nodes marked as verbose are hidden.
215+
* Otherwise, only control nodes marked as verbose are hidden (because they
216+
* are disconnected).
217+
*
218+
* @param dn
219+
*/
220+
private boolean isHidden(Node dn) {
221+
if( verbose )
222+
return dn.verbose && dn.type == Node.Type.CONTROL;
223+
else
224+
return dn.verbose;
225+
}
226+
227+
/**
228+
* When verbose mode is disabled, subgraphs with no visible nodes
229+
* are hidden. Otherwise, only subgraphs with no nodes are hidden
230+
* (because they are disconnected).
231+
*
232+
* @param dn
233+
*/
234+
private boolean isHidden(Subgraph s) {
235+
if( verbose )
236+
return s.nodes.isEmpty();
237+
else
238+
return s.isVerbose();
239+
}
240+
206241
}

0 commit comments

Comments
 (0)