Skip to content

Commit 4dfaa9b

Browse files
authored
Recipe: Dependency loop detection (#240)
1 parent 6f5aaa3 commit 4dfaa9b

File tree

3 files changed

+340
-0
lines changed

3 files changed

+340
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: Validation
3+
weight: 150
4+
---
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
---
2+
title: "Dependency Loops"
3+
weight: 100
4+
---
5+
6+
## What is the problem?
7+
8+
If you are building some composite data structures or have some computation flow, you might be interested whether the product, that you are generating, does contain any loops back to the already used product. We want to implement a validation that does this detection in the background and notifies us by highlighting the lines causing this problem.
9+
The second part of the problem is the resolution of the dependencies. If there are no loops, we want to get a resolution of the dependencies in a loop-free order.
10+
11+
Examples for such dependency loops are:
12+
13+
* For data structures you could think of a structure in C that references itself (without using the pointer notation). This would lead to an infinitely expanding data type, which is practically not doable.
14+
15+
```c
16+
typedef struct {
17+
int value;
18+
LinkedList next; //error, should be "MyStruct* next;", a pointer to next
19+
} LinkedList;
20+
```
21+
22+
* Or for control flows these loops can be interpreted as recursion detection, a function that calls itself (with any number of function calls to other functions in-between).
23+
24+
```c
25+
void foo() {
26+
bar();
27+
}
28+
void bar() {
29+
answer42();
30+
}
31+
void answer42() {
32+
bar(); //error, foo calls bar, bar calls answer42, answer42 calls foo
33+
}
34+
```
35+
36+
Regardless of what usecase you have, you might have an interest to detect those loops and get early feedback in the shape of a validation error.
37+
38+
The other point is the **resolution for loop-free dependencies**. Think of a net of package imports in
39+
a programming language. You want to know the order in which you can import the packages without getting into trouble.
40+
41+
```plaintext
42+
A -> B -> C
43+
A -> C
44+
C -> D
45+
46+
//resolution: A, B, C, D
47+
```
48+
49+
Or think of a function call graph. You want to know the order in which you can build the functions such that every dependency was built before the dependent function.
50+
51+
```c
52+
void answer42() {
53+
printf("42\n");
54+
}
55+
void bar1() {
56+
answer42();
57+
}
58+
void foo() {
59+
bar1();
60+
bar2();
61+
}
62+
void bar2() {
63+
answer42();
64+
}
65+
```
66+
67+
Here the resolution would be: `answer42`, `bar1`, `bar2`, `foo`.
68+
69+
## How to solve it?
70+
71+
There are two approaches for a loop detection and the loop-free resolution depending on the nature of your situation.
72+
73+
### Simple nature
74+
75+
#### Simple detection
76+
77+
If you have a `1:n` relationship like the `super class`-to-`sub class` relation for classes, you can do it by simply walking along the parent route (or in this specific example the `super class`-route). Just keep in mind all visited places and if one parent is already in that list, you have detected a loop!
78+
79+
```java
80+
public class A extends B {}
81+
public class B extends C {}
82+
public class C extends A {} //error
83+
```
84+
85+
#### Simple resolution
86+
87+
Assuming that you have no loops back, you can resolve a list of dependencies.
88+
You do a simple depth-first-search, starting with the parent visiting the children afterwards (recursively).
89+
90+
```java
91+
public class A {} //add A
92+
public class B extends A {} //add A -> B
93+
public class C extends A {} //add A -> C
94+
public class D extends C {} //add C -> D
95+
public class E extends C {} //add C -> E
96+
97+
//resolution: A, (B or (C, (D or E)))
98+
```
99+
100+
### Complex nature
101+
102+
#### Complex detection
103+
104+
If you have a `n:m` relationship like it is given for function calls (a function can be called by `m` function and can call `n` functions), you can solve the question for loops by creating a directed graph.
105+
106+
In this example the nodes are the set of all functions and function calls are stored as edges (for each call from function A to every function B).
107+
The key algorithm is the search for the so-called strongly-connected components in the resulting graph.
108+
109+
Please use an existing solution for the algorithm (keep in mind the effort you can avoid and a quality you can gain)! The algorithm is able to output every loop with all its members of that loop. But you are free to make your own implementation.
110+
111+
#### Complex resolution
112+
113+
The directed graph approach can be processed further when there were no loops found:
114+
115+
With a "topological sort" you can get an order that respects all dependencies. Means more or less: You start with the node that has no dependencies, remove it, put it into your sorted list and do the same for the resulting graph again and again until all dependencies were resolved.
116+
117+
The topological sort (as well as the strongly-connected component search) is a standard algorithm in every good graph library.
118+
119+
## How to make it work in Langium?
120+
121+
In the following example we will resolve the dependencies for a complex nature of data.
122+
123+
Therefore we will take the `HelloWorld` example from the learning section and extend it with a validation that checks for greeting loops. Greeting loops are forbidden in this example. When `A` greets `B` and `B` greets `C`, then `C` must not greet `A`.
124+
125+
### Adapt the grammar
126+
127+
We will change the `HelloWorld` grammar, so that persons can greet each other. After that, we will introduce a validation in order to forbid "greeting loops".
128+
129+
```langium
130+
grammar HelloWorld
131+
132+
entry Model:
133+
(persons+=Person | greetings+=Greeting)*;
134+
135+
Person:
136+
'person' name=ID;
137+
138+
Greeting:
139+
greeter=[Person:ID] 'greets' greeted=[Person:ID] '!';
140+
141+
hidden terminal WS: /\s+/;
142+
terminal ID: /[_a-zA-Z][\w_]*/;
143+
````
144+
145+
After the change build your grammar with `npm run langium:generate`.
146+
147+
### Loop detection
148+
149+
Now we will add the validation. Here we will use the graph library ‚graphology‘. Please install these three packages (`graphology` contains the data structure, `graphology-components` contains the strongly-connected component search, `graphology-dag` contains the topological sort):
150+
151+
```bash
152+
npm install graphology graphology-components graphology-dag
153+
```
154+
155+
Open the `hello-world-validator.ts` and add another validator for `Model`. It is important to say that we do not create a check on the `Greeting` level, because we need the overview over all greetings. The complete overview is given for the `Model` AST node. It would be possible to just calculate cycles for a single greeting or person, but that is more complex and less performant!
156+
157+
```typescript
158+
const checks: ValidationChecks<HelloWorldAstType> = {
159+
Model: validator.checkGreetingCycles, // new!!!
160+
Person: validator.checkPersonStartsWithCapital
161+
};
162+
```
163+
164+
And here is the implementation:
165+
166+
```typescript
167+
checkGreetingCycles(model: Model, accept: ValidationAcceptor): void {
168+
//arrange the graph
169+
const graph = new DirectedGraph<{}, {greeting: Greeting}>();
170+
model.persons.forEach(person => {
171+
graph.addNode(person.name);
172+
})
173+
model.greetings.forEach(greeting => {
174+
if(greeting.greeter.ref && greeting.greeted.ref && !graph.hasDirectedEdge(greeting.greeter.ref.name, greeting.greeted.ref.name)) {
175+
graph.addEdge(greeting.greeter.ref.name, greeting.greeted.ref.name, {
176+
greeting //we store the greeting for later reference in the validation message
177+
});
178+
}
179+
});
180+
181+
//compute the components
182+
const components = stronglyConnectedComponents(graph);
183+
184+
//evaluate result (filter out size-1-components)
185+
const actualLoops = components.filter(c => c.length > 1);
186+
for (const component of actualLoops) {
187+
const set = new Set<string>(component);
188+
//for each node in the component...
189+
for (const from of set) {
190+
//check whether the out edges...
191+
for (const { target: to, attributes: { greeting } } of graph.outEdgeEntries(from)) {
192+
//are within the component
193+
if(set.has(to)) {
194+
//if yes, set an error on the corresponding greeting
195+
accept("error", "Greeting loop detected!", {
196+
node: greeting
197+
});
198+
}
199+
}
200+
}
201+
}
202+
}
203+
```
204+
205+
After finishing your validator, do not forget to build your project with `npm run build`.
206+
So a `.hello` file like this one, would have 3 greetings with an error:
207+
208+
```plaintext
209+
person Homer
210+
person Marge
211+
person Pinky
212+
person Brain
213+
214+
Homer greets Marge! //error
215+
Marge greets Brain! //error
216+
Brain greets Homer! //error
217+
Pinky greets Marge!
218+
```
219+
220+
Here is the screenshot of VS Code with the error:
221+
222+
![Greeting loop errors](/assets/dependency-loops.png)
223+
224+
### Dependency resolution
225+
226+
The topological sort can be done like this:
227+
228+
```typescript
229+
import { topologicalSort } from 'graphology-dag';
230+
231+
//resolvedOrder is an array of person names!
232+
const resolvedOrder = topologicalSort(graph);
233+
```
234+
235+
This will give you back an order of greeters. The rule would be like: `You can only greet if every greeting addressed to you was already spoken out.`
236+
For a `.hello` file like this, we would get the order: `Homer`, `Brain`, `Pinky`, `Marge`.
237+
238+
```plaintext
239+
person Homer
240+
person Marge
241+
242+
person Pinky
243+
person Brain
244+
245+
Homer greets Marge!
246+
Brain greets Pinky!
247+
Pinky greets Marge!
248+
```
249+
250+
* `Homer` is not greeted by anyone, so he can start greeting `Marge`.
251+
* `Marge` and `Pinky` are blocked by `Pinky` and `Brain`.
252+
* `Brain` is the next and unblocks `Pinky`.
253+
* After `Pinky` is done, `Marge` is unblocked as well.
254+
* But `Marge` has no one to greet.
255+
* So, we are done.
256+
257+
## Appendix
258+
259+
<details>
260+
<summary>Full Implementation</summary>
261+
262+
```ts
263+
import type { ValidationAcceptor, ValidationChecks } from 'langium';
264+
import type { Greeting, HelloWorldAstType, Model } from './generated/ast.js';
265+
import type { HelloWorldServices } from './hello-world-module.js';
266+
import { DirectedGraph } from 'graphology';
267+
import { stronglyConnectedComponents } from 'graphology-components';
268+
import { topologicalSort } from 'graphology-dag';
269+
270+
/**
271+
* Register custom validation checks.
272+
*/
273+
export function registerValidationChecks(services: HelloWorldServices) {
274+
const registry = services.validation.ValidationRegistry;
275+
const validator = services.validation.HelloWorldValidator;
276+
const checks: ValidationChecks<HelloWorldAstType> = {
277+
Model: validator.checkGreetingCycles,
278+
//Not needed for this example
279+
//Person: validator.checkPersonStartsWithCapital
280+
};
281+
registry.register(checks, validator);
282+
}
283+
284+
/**
285+
* Implementation of custom validations.
286+
*/
287+
export class HelloWorldValidator {
288+
checkGreetingCycles(model: Model, accept: ValidationAcceptor): void {
289+
//arrange the graph
290+
const graph = new DirectedGraph<{}, {greeting: Greeting}>();
291+
model.persons.forEach(person => {
292+
graph.addNode(person.name);
293+
})
294+
model.greetings.forEach(greeting => {
295+
if(greeting.greeter.ref && greeting.greeted.ref && !graph.hasDirectedEdge(greeting.greeter.ref.name, greeting.greeted.ref.name)) {
296+
graph.addEdge(greeting.greeter.ref.name, greeting.greeted.ref.name, {
297+
greeting
298+
});
299+
}
300+
});
301+
302+
//compute the components
303+
const components = stronglyConnectedComponents(graph);
304+
305+
//evaluate result (filter out size-1-components)
306+
const actualLoops = components.filter(c => c.length > 1);
307+
for (const component of actualLoops) {
308+
const set = new Set<string>(component);
309+
//for each node in the component...
310+
for (const from of set) {
311+
//check whether the out edges...
312+
for (const { target: to, attributes: { greeting } } of graph.outEdgeEntries(from)) {
313+
//are within the component
314+
if(set.has(to)) {
315+
//if yes, set an error on the corresponding greeting
316+
accept("error", "Greeting loop detected!", {
317+
node: greeting
318+
});
319+
}
320+
}
321+
}
322+
}
323+
324+
//resolve all dependencies
325+
if(actualLoops.length === 0) {
326+
const resolvedOrder = topologicalSort(graph);
327+
//this is done as a hint, just for demonstration purposes
328+
accept('hint', "Please greet in the following greeter order: "+resolvedOrder.join(", "), {
329+
node: model
330+
});
331+
}
332+
}
333+
}
334+
```
335+
336+
</details>
156 KB
Loading

0 commit comments

Comments
 (0)