🚧 WIP - Building the cycles graph by taking into account parent rule

This contextualization will allow to have a (more complex) graph that
will contain the information of the parent rule of the current rule.
This will allow calling `getApplicableReplacedBy` and thus remove the
flattening logic, which was imperfect.

On the other hand, this needs to make recursive calls to
`ruleDepsOfRule` in case of a reference node, and thus make the graph
much fatter.

Approx TODO (see `ruleDependencies.ts`):

- [ ] refactor to propagate the `parentRuleNode` in the rule
      dependencies
- [ ] build recursive calls into `ruleDepsOfReference`
cycles-detection-with-context
Alexandre Hajjar 2020-10-01 12:15:43 +02:00
parent bdef762278
commit d705e7e047
4 changed files with 26 additions and 181 deletions

View File

@ -12,7 +12,7 @@ type GraphCycles = Array<Array<GraphNodeRepr>>
export class GraphError extends Error {}
export function buildNaiveDependenciesGraph<Names extends string>(
export function buildDependenciesGraph<Names extends string>(
rulesDeps: RulesDependencies<Names>,
assertNoMultiDependency = false
): graphlib.Graph {
@ -44,110 +44,15 @@ export function buildNaiveDependenciesGraph<Names extends string>(
return g
}
/**
* If a part of a graph is like:
* A -formule-> B
* B -replacedBy-> A
* (what is called a "remplace one-level loop" or "ROLL")
* then split it in two separated sub-graphs
* Af -formule-> Bf
* Br -replacedBy-> Ar
* and re-plug the other edges related to A and B on Af, Bf, Ar and Br.
*
* This operation is destroying the initial loop, and corresponds closely to
* the behavior of the `parseReference.js:getApplicableReplacedBy` function.
*/
export function flattenOneLevelRemplaceLoops(naiveGraph: graphlib.Graph) {
const replacedByEdges = naiveGraph
.edges()
.filter(e => naiveGraph.edge(e).type == DependencyType.replacedBy)
const ROLLEdges = replacedByEdges.flatMap(e => {
// Note: there is at max one such reverse edge, because graphlib doesn't
// store more than one edge with the same in and out.
const reverseEdges = naiveGraph.inEdges(e.v, e.w)
if (
reverseEdges.length > 0 &&
naiveGraph.edge(reverseEdges[0]).type == DependencyType.formule
) {
return [e, reverseEdges[0]]
} else {
return []
}
})
// Map with default Set values:
const ROLLNodesTypes = new Proxy(
{},
{
get: (target, name) =>
name in target ? target[name] : (target[name] = new Set())
}
)
ROLLEdges.forEach(e => {
ROLLNodesTypes[e.v].add(naiveGraph.edge(e).type)
ROLLNodesTypes[e.w].add(naiveGraph.edge(e).type)
})
const flattenedGraph = new graphlib.Graph()
const specifyNodeName = (depType, nodeName) =>
`${nodeName} [depType: ${depType}]`
naiveGraph.edges().forEach(e => {
// Nodes which are forming the one-level remplace loop: duplicate them so as
// to have 2 independent sub-graphs: one representing the formule
// dependency, the other representing the remplace dependency.
if (ROLLEdges.includes(e)) {
const edgeType = naiveGraph.edge(e).type
flattenedGraph.setEdge(
specifyNodeName(edgeType, e.v),
specifyNodeName(edgeType, e.w),
{ type: edgeType }
)
}
// Edges which are incoming or outgoing of the loop nodes: duplicate them on
// sub-graphs.
// Outgoing:
else if (e.v in ROLLNodesTypes) {
ROLLNodesTypes[e.v].forEach(depType => {
flattenedGraph.setEdge(specifyNodeName(depType, e.v), e.w, {
type: naiveGraph.edge(e).type
})
})
}
// Incoming:
else if (e.w in ROLLNodesTypes) {
// [XXX] To remove:
if (e.v in ROLLNodesTypes) {
throw new Error('shouldnt happen')
}
ROLLNodesTypes[e.w].forEach(depType => {
flattenedGraph.setEdge(e.v, specifyNodeName(depType, e.w), {
type: naiveGraph.edge(e).type
})
})
}
// Edges which are un-related: just copy them.
else {
flattenedGraph.setEdge(e.v, e.w, { type: naiveGraph.edge(e).type })
}
})
return flattenedGraph
}
export function cyclicDependencies<Names extends string>(
rawRules: Rules<Names> | string,
assertNoMultiDependency = false
): GraphCycles {
const parsedRules = parseRules(rawRules)
const rulesDependencies = buildRulesDependencies(parsedRules)
const naiveGraph = buildNaiveDependenciesGraph(
const dependenciesGraph = buildDependenciesGraph(
rulesDependencies,
assertNoMultiDependency
)
const flattenedGraph = flattenOneLevelRemplaceLoops(naiveGraph)
return graphlib.alg.findCycles(flattenedGraph)
return graphlib.alg.findCycles(dependenciesGraph)
}

View File

@ -1,5 +1,6 @@
import * as R from 'ramda'
import { ParsedRules } from '../types'
import { getApplicableReplacedBy } from '../parseReference'
import * as ASTTypes from './ASTTypes'
export enum DependencyType {
@ -64,6 +65,16 @@ export function ruleDepsOfNode<Names extends string>(
function ruleDepsOfReference(
reference: ASTTypes.Reference<Names>
): RuleDependencies<Names> {
// [XXX] Todo:
// - build a context stack (before) and pass it over in ruleDepsOfNode and
// this sub-function
// - take a look at all other sub-functions like RecalculMech to check if
// they need the same treatment
// - modify the graph node labels: hash(ruleName, context stack)
// - here, push the (current) ruleName to the stack
// - call ruleDepsOfRuleNode on (reference.dottedName, newStack)
// - rewire accordingly
// - and indeed in the function make the call to getApplicableReplacedBy
return [[reference.dottedName, DependencyType.formule]]
}
@ -394,7 +405,8 @@ export function ruleDepsOfNode<Names extends string>(
}
function ruleDepsOfRuleNode<Names extends string>(
rule: ASTTypes.RuleNode<Names>
rule: ASTTypes.RuleNode<Names>,
parentRuleName: Names | ''
): RuleDependencies<Names> {
const subNodes = [
rule.formule,
@ -402,15 +414,16 @@ function ruleDepsOfRuleNode<Names extends string>(
rule['non applicable si']
].filter(x => x !== undefined) as Array<ASTTypes.ASTNode>
const subNodesDeps = subNodes
.map(x => ruleDepsOfNode<Names>(rule.dottedName, x))
.map(subNode => ruleDepsOfNode<Names>(rule.dottedName, subNode))
.flat(1)
const isDisabledByDependencies: RuleDependencies<Names> = rule.isDisabledBy.map(
x => [x.dottedName, DependencyType.isDisabledBy]
)
const replacedByDependencies: RuleDependencies<Names> = rule.replacedBy.map(
x => [x.referenceNode.dottedName, DependencyType.replacedBy]
)
const replacedByDependencies: RuleDependencies<Names> = getApplicableReplacedBy(
parentRuleName,
rule.replacedBy
).map(x => [x.referenceNode.dottedName, DependencyType.replacedBy])
return [subNodesDeps, isDisabledByDependencies, replacedByDependencies].flat(
1
)
@ -433,6 +446,7 @@ export function buildRulesDependencies<Names extends string>(
([dottedName, ruleNode]: [Names, ASTTypes.RuleNode<Names>]): [
Names,
RuleDependencies<Names>
] => [dottedName, ruleDepsOfRuleNode<Names>(ruleNode)]
// [XXX] Check how the root nodes are contextRuleName'd:
] => [dottedName, ruleDepsOfRuleNode<Names>(ruleNode, '')]
)
}

View File

@ -30,7 +30,6 @@ export const getApplicableReplacedBy = (contextRuleName, replacedBy) =>
!blackListedNames ||
blackListedNames.every(name => !contextRuleName.startsWith(name))
)
// ⚠️ this behavior is referenced in `cyclesLib/graph.ts`
.filter(({ referenceNode }) => contextRuleName !== referenceNode.dottedName)
/**

View File

@ -1,12 +1,8 @@
import yaml from 'yaml'
import { expect } from 'chai'
import dedent from 'dedent-js'
import graphlib from '@dagrejs/graphlib'
import { DependencyType } from '../source/cyclesLib/rulesDependencies'
import {
cyclicDependencies,
flattenOneLevelRemplaceLoops,
GraphError
} from '../source/cyclesLib/graph'
import { cyclicDependencies, GraphError } from '../source/cyclesLib/graph'
import Engine from '../source/index'
describe('Naive dependencies builder', () => {
it('catches double-dependecies', () => {
@ -21,75 +17,6 @@ describe('Naive dependencies builder', () => {
})
})
describe('ROLL flatten-o-tron 2500 ™', () => {
it('should replace 2 ROLL nodes with 4 nodes without loop', () => {
const g = new graphlib.Graph()
g.setEdge('b', 'c', { type: DependencyType.formule })
g.setEdge('c', 'b', { type: DependencyType.replacedBy })
const flattenedGraph = flattenOneLevelRemplaceLoops(g)
expect(flattenedGraph.nodes()).to.deep.equal([
'b [depType: 0]',
'c [depType: 0]',
'c [depType: 1]',
'b [depType: 1]'
])
expect(flattenedGraph.edges()).to.deep.equal([
{ v: 'b [depType: 0]', w: 'c [depType: 0]' },
{ v: 'c [depType: 1]', w: 'b [depType: 1]' }
])
})
it('should replace 2 ROLL nodes in context of a larger graph', () => {
const g = new graphlib.Graph()
g.setEdge('a', 'b', { type: DependencyType.formule })
g.setEdge('a', 'c', { type: DependencyType.formule })
g.setEdge('b', 'c', { type: DependencyType.formule })
g.setEdge('c', 'b', { type: DependencyType.replacedBy })
g.setEdge('b', 'd', { type: DependencyType.formule })
g.setEdge('c', 'd', { type: DependencyType.formule })
const flattenedGraph = flattenOneLevelRemplaceLoops(g)
expect(flattenedGraph.nodes()).to.deep.equal([
'a',
'b [depType: 1]',
'b [depType: 0]',
'c [depType: 1]',
'c [depType: 0]',
'd'
])
expect(flattenedGraph.edges()).to.have.deep.members([
{ v: 'a', w: 'b [depType: 0]' },
{ v: 'a', w: 'b [depType: 1]' },
{ v: 'a', w: 'c [depType: 0]' },
{ v: 'a', w: 'c [depType: 1]' },
{ v: 'b [depType: 0]', w: 'c [depType: 0]' },
{ v: 'c [depType: 1]', w: 'b [depType: 1]' },
{ v: 'b [depType: 0]', w: 'd' },
{ v: 'b [depType: 1]', w: 'd' },
{ v: 'c [depType: 0]', w: 'd' },
{ v: 'c [depType: 1]', w: 'd' }
])
})
it('should not replace any nodes in a 2-level formule + remplace loop', () => {
const g = new graphlib.Graph()
g.setEdge('a', 'b', { type: DependencyType.formule })
g.setEdge('b', 'c', { type: DependencyType.formule })
g.setEdge('c', 'a', { type: DependencyType.replacedBy })
const flattenedGraph = flattenOneLevelRemplaceLoops(g)
expect(flattenedGraph.nodes()).to.deep.equal(['a', 'b', 'c'])
expect(flattenedGraph.edges()).to.deep.equal(g.edges())
})
})
describe('Cyclic dependencies detectron 3000 ™', () => {
it('should detect the trivial formule cycle', () => {
const rules = dedent`