Skip to content

Commit 66c5dde

Browse files
committed
Cycles throw a VtlScriptException
1 parent b6c7483 commit 66c5dde

File tree

4 files changed

+84
-39
lines changed

4 files changed

+84
-39
lines changed

vtl-engine/src/main/java/fr/insee/vtl/engine/VtlScriptEngine.java

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import fr.insee.vtl.engine.exceptions.VtlRuntimeException;
66
import fr.insee.vtl.engine.exceptions.VtlSyntaxException;
7+
import fr.insee.vtl.engine.utils.dag.DAGBuilder;
78
import fr.insee.vtl.engine.utils.dag.DAGStatement;
89
import fr.insee.vtl.engine.visitors.AssignmentVisitor;
910
import fr.insee.vtl.engine.visitors.DAGBuildingVisitor;
@@ -287,28 +288,32 @@ public void syntaxError(
287288
* @param start Root of parsetree
288289
* @return reordered VTL
289290
*/
290-
private VtlParser.StartContext reorderScript(VtlParser.StartContext start) {
291-
DAGBuildingVisitor visitor = new DAGBuildingVisitor();
292-
List<DAGStatement> sortedStatements = visitor.visit(start);
293-
294-
VtlParser.StartContext startReordered =
295-
new VtlParser.StartContext((ParserRuleContext) start.getRuleContext(), start.invokingState);
296-
297-
// Build a set of unsorted indices that need reordering
298-
Set<Integer> sortedIndices = sortedStatements.stream()
299-
.map(DAGStatement::unsortedIndex)
300-
.collect(Collectors.toSet());
301-
302-
int sortedIndex = 0;
303-
for (int i = 0; i < start.getChildCount(); i++) {
304-
if (sortedIndices.contains(i)) {
305-
DAGStatement stmt = sortedStatements.get(sortedIndex++);
306-
startReordered.addAnyChild(start.getChild(stmt.unsortedIndex()));
307-
} else {
308-
startReordered.addAnyChild(start.getChild(i));
309-
}
291+
private VtlParser.StartContext reorderScript(VtlParser.StartContext start)
292+
throws VtlScriptException {
293+
DAGBuildingVisitor visitor = new DAGBuildingVisitor();
294+
List<DAGStatement> unsortedStatements = visitor.visit(start);
295+
296+
// Create DAG & topological sort
297+
DAGBuilder dagBuilder = new DAGBuilder(unsortedStatements, start);
298+
List<DAGStatement> sortedStatements = dagBuilder.topologicalSortedStatements();
299+
300+
VtlParser.StartContext startReordered =
301+
new VtlParser.StartContext((ParserRuleContext) start.getRuleContext(), start.invokingState);
302+
303+
// Build a set of unsorted indices that need reordering
304+
Set<Integer> sortedIndices =
305+
sortedStatements.stream().map(DAGStatement::unsortedIndex).collect(Collectors.toSet());
306+
307+
int sortedIndex = 0;
308+
for (int i = 0; i < start.getChildCount(); i++) {
309+
if (sortedIndices.contains(i)) {
310+
DAGStatement stmt = sortedStatements.get(sortedIndex++);
311+
startReordered.addAnyChild(start.getChild(stmt.unsortedIndex()));
312+
} else {
313+
startReordered.addAnyChild(start.getChild(i));
310314
}
311-
return startReordered;
315+
}
316+
return startReordered;
312317
}
313318

314319
/**

vtl-engine/src/main/java/fr/insee/vtl/engine/utils/dag/DAGBuilder.java

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
package fr.insee.vtl.engine.utils.dag;
22

3+
import fr.insee.vtl.engine.VtlScriptEngine;
4+
import fr.insee.vtl.model.exceptions.VtlScriptException;
5+
import fr.insee.vtl.parser.VtlParser;
36
import java.util.*;
7+
import java.util.stream.Collectors;
8+
import org.antlr.v4.runtime.tree.ParseTree;
9+
import org.antlr.v4.runtime.tree.RuleNode;
10+
import org.antlr.v4.runtime.tree.TerminalNode;
411
import org.jgrapht.Graph;
12+
import org.jgrapht.alg.cycle.CycleDetector;
513
import org.jgrapht.graph.DefaultDirectedGraph;
614
import org.jgrapht.graph.DefaultEdge;
715
import org.jgrapht.traverse.TopologicalOrderIterator;
816

917
/** Helper Class to build a DAG from VTL */
1018
public class DAGBuilder {
1119

12-
private final Graph<DAGStatement, DefaultEdge> graph;
20+
private final Graph<DAGStatement, DefaultEdge> graph;
21+
private final VtlParser.StartContext startContext;
1322

14-
public DAGBuilder(List<DAGStatement> statements) {
15-
graph = buildDAG(statements);
16-
}
23+
public DAGBuilder(List<DAGStatement> statements, VtlParser.StartContext startContext) {
24+
graph = buildDAG(statements);
25+
this.startContext = startContext;
26+
}
1727

1828
/**
1929
* Builds a DAG from a list of VTL statements
@@ -44,9 +54,43 @@ private static boolean dependsOn(DAGStatement stmt2, DAGStatement stmt1) {
4454
return stmt2.consumes().contains(produced);
4555
}
4656

47-
public List<DAGStatement> topologicalSortedStatements() {
48-
final List<DAGStatement> topologicalSorted = new ArrayList<>();
49-
new TopologicalOrderIterator<>(graph).forEachRemaining(topologicalSorted::add);
50-
return topologicalSorted;
57+
public List<DAGStatement> topologicalSortedStatements() throws VtlScriptException {
58+
Optional<VtlScriptException> cycleError = checkForCycles();
59+
if (cycleError.isPresent()) {
60+
throw cycleError.get();
61+
}
62+
final List<DAGStatement> topologicalSorted = new ArrayList<>();
63+
new TopologicalOrderIterator<>(graph).forEachRemaining(topologicalSorted::add);
64+
return topologicalSorted;
65+
}
66+
67+
private Optional<VtlScriptException> checkForCycles() {
68+
CycleDetector<DAGStatement, DefaultEdge> cycleDetector = new CycleDetector<>(graph);
69+
Set<DAGStatement> statementsInCycle = cycleDetector.findCycles();
70+
if (statementsInCycle.isEmpty()) {
71+
return Optional.empty();
5172
}
73+
String dagStatementsInCycle =
74+
statementsInCycle.stream()
75+
.map(d -> parseTreeToText(startContext.getChild(d.unsortedIndex())))
76+
.collect(Collectors.joining(";", "", ""));
77+
return Optional.of(
78+
new VtlScriptException(
79+
"Cycle detected in Script. The following statements form at least one cycle: "
80+
+ dagStatementsInCycle,
81+
VtlScriptEngine.fromContext(startContext)));
82+
}
83+
84+
private String parseTreeToText(ParseTree child) {
85+
StringBuilder result = new StringBuilder();
86+
if (child instanceof TerminalNode) {
87+
result.append(child.getText());
88+
result.append(" ");
89+
} else if (child instanceof RuleNode && child.getChildCount() != 0) {
90+
for (int i = 0; i < child.getChildCount(); ++i) {
91+
result.append(parseTreeToText(child.getChild(i)));
92+
}
93+
}
94+
return result.toString();
95+
}
5296
}

vtl-engine/src/main/java/fr/insee/vtl/engine/visitors/DAGBuildingVisitor.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package fr.insee.vtl.engine.visitors;
22

3-
import fr.insee.vtl.engine.utils.dag.DAGBuilder;
43
import fr.insee.vtl.engine.utils.dag.DAGStatement;
54
import fr.insee.vtl.parser.VtlBaseVisitor;
65
import fr.insee.vtl.parser.VtlParser;
@@ -21,10 +20,7 @@ public List<DAGStatement> visitChildren(RuleNode node) {
2120
@Override // explicit call to super, as visiting children is only supported for the start node
2221
// (only top level statements can be reordered)
2322
public List<DAGStatement> visitStart(VtlParser.StartContext node) {
24-
final List<DAGStatement> unsortedStatements = super.visitChildren(node);
25-
// Create DAG & topological sort
26-
DAGBuilder dagBuilder = new DAGBuilder(unsortedStatements);
27-
return dagBuilder.topologicalSortedStatements();
23+
return super.visitChildren(node);
2824
}
2925

3026
// Extract statements that can be reordered

vtl-engine/src/test/java/fr/insee/vtl/engine/utils/dag/DagTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import javax.script.ScriptEngine;
1616
import javax.script.ScriptEngineManager;
1717
import javax.script.ScriptException;
18-
import org.jgrapht.traverse.NotDirectedAcyclicGraphException;
1918
import org.junit.jupiter.api.BeforeEach;
2019
import org.junit.jupiter.api.Test;
2120
import org.junit.jupiter.params.ParameterizedTest;
@@ -113,15 +112,16 @@ public void testDagSimpleExampleWithReordering() throws ScriptException {
113112
}
114113

115114
@Test
116-
public void testDagCircle() {
115+
public void testDagCycle() {
117116
ScriptContext context = engine.getContext();
118117
context.setAttribute("a", 1L, ScriptContext.ENGINE_SCOPE);
119118
assertThatThrownBy(
120119
() -> {
121-
engine.eval("b := a; c := b; a := c;");
120+
engine.eval("e := a; b := a; c := b; a := c; f := a;");
122121
})
123-
.isInstanceOf(NotDirectedAcyclicGraphException.class)
124-
.hasMessage("Graph is not a DAG");
122+
.isInstanceOf(VtlScriptException.class)
123+
.hasMessage(
124+
"Cycle detected in Script. The following statements form at least one cycle: b := a ;c := b ;a := c ");
125125
}
126126

127127
@Test

0 commit comments

Comments
 (0)