Skip to content

Commit

Permalink
fix(optim): better handling of contexte rules
Browse files Browse the repository at this point in the history
  • Loading branch information
EmileRolley committed Feb 12, 2024
1 parent c9e2676 commit 7ae9db1
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 58 deletions.
80 changes: 43 additions & 37 deletions source/optims/constantFolding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@ type FoldingCtx = {
unfoldableRules: Set<RuleName>
}

function addMapEntry(map: RefMap, key: RuleName, values: RuleName[]) {
let vals = map.get(key)
if (vals) {
values.forEach((val) => vals.add(val))
}
map.set(key, vals || new Set(values))
function addMapEntry(map: RefMap, key: RuleName, values: Set<RuleName>) {
const vals = map.get(key) ?? new Set()
values.forEach((val) => vals.add(val))
map.set(key, vals)
}

function initFoldingCtx(
Expand All @@ -70,10 +68,6 @@ function initFoldingCtx(
const unfoldableRules = new Set<RuleName>()
const parsedRules = copyFullParsedRules(engine)

// NOTE: we need to traverse the AST to find all the references of a rule.
// We can't use the [referencesMap] from the engine's context because it
// contains references to rules that are beyond the scope of the current
// rule.
for (const ruleName in parsedRules) {
const ruleNode = parsedRules[ruleName]

Expand All @@ -88,24 +82,42 @@ function initFoldingCtx(
})
}

const reducedAST =
if (ruleNode.explanation.valeur.nodeKind === 'contexte') {
engine.cache.traversedVariablesStack = []
const evaluation = engine.evaluate(ruleName)

// We don't want to fold a rule which can be nullable with a different situation
// For example, if its namespace is conditionnaly applicable.
//
// TODO(@EmileRolley): for now, all ref nodes inside a contexte are considered
// as not foldable. We could be more precise by associating a ref node with
// the rules that are used in its contexte therefore we could fold the ref node
// in all other cases.
if (
Object.keys(evaluation.missingVariables).length !== 0 ||
isNullable(evaluation)
) {
unfoldableRules.add(ruleName)
ruleNode.explanation.valeur.explanation.contexte.forEach(([ref, _]) => {
unfoldableRules.add(ref.dottedName)
})
}
}

const traversedRefs: Set<RuleName> =
// We need to traverse the AST to find all the references used inside a rule.
//
// NOTE: We can't use the [referencesMap] from the engine's context because it
// contains references to rules that are beyond the scope of the current
// rule and we only want to consider the references that are used inside the
// current rule.
reduceAST(
(acc: Set<RuleName>, node: ASTNode) => {
if (node.nodeKind === 'contexte') {
const { missingVariables } = engine.evaluateNode(node)

// We can't fold it
if (Object.keys(missingVariables).length !== 0) {
unfoldableRules.add(ruleName)
node.explanation.contexte.forEach(([ref, _]) => {
unfoldableRules.add(ref.dottedName)
})
}
}
if (
node.nodeKind === 'reference' &&
'dottedName' in node &&
node.dottedName !== ruleName
node.dottedName !== ruleName &&
!node.dottedName.endsWith('$SITUATION')
) {
return acc.add(node.dottedName)
}
Expand All @@ -114,14 +126,10 @@ function initFoldingCtx(
ruleNode.explanation.valeur,
) ?? new Set()

const traversedVariables: RuleName[] = Array.from(reducedAST).filter(
(name) => !name.endsWith('$SITUATION'),
)

if (traversedVariables.length > 0) {
addMapEntry(refs.childs, ruleName, traversedVariables)
traversedVariables.forEach((traversedVar) => {
addMapEntry(refs.parents, traversedVar, [ruleName])
if (traversedRefs.size > 0) {
addMapEntry(refs.childs, ruleName, traversedRefs)
traversedRefs.forEach((traversedVar) => {
addMapEntry(refs.parents, traversedVar, new Set([ruleName]))
})
}
}
Expand Down Expand Up @@ -163,12 +171,14 @@ function isEmptyRule(rule: RuleNode): boolean {
return Object.keys(rule.rawNode).length === 0
}

/** Replaces all references in parent refs of [ruleName] by its [constantNode] */
/**
* Replaces all references in parent refs of [ruleName] by its [constantNode]
*/
function searchAndReplaceConstantValueInParentRefs(
ctx: FoldingCtx,
ruleName: RuleName,
constantNode: ASTNode,
): void {
) {
const refs = ctx.refs.parents.get(ruleName)

if (refs) {
Expand Down Expand Up @@ -366,11 +376,7 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void {
nodeValue,
unit,
)

searchAndReplaceConstantValueInParentRefs(ctx, ruleName, constantNode)
if (ctx.parsedRules[ruleName] === undefined) {
return
}

const childs = ctx.refs.childs.get(ruleName) ?? new Set()

Expand Down
93 changes: 72 additions & 21 deletions test/optims/constantFolding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,48 @@ describe('Constant folding [base]', () => {
expect(constantFoldingWith(rawRules)).toEqual(rawRules)
})

it('should fold rules impacted by a [contexte] with multiple contexte rules', () => {
it('should fold constant rules used in a [contexte]', () => {
const rawRules = {
root: {
valeur: 'rule to recompute',
contexte: {
'rule to replace': 'constant',
},
},
'rule to recompute': {
valeur: '(rule to replace * 2) * question',
},
'rule to replace': {
valeur: 0,
},
question: {
question: 'Question ?',
},
constant: {
valeur: 10,
},
}
expect(constantFoldingWith(rawRules)).toEqual({
root: {
valeur: 'rule to recompute',
contexte: {
'rule to replace': 10,
},
optimized: 'partially',
},
'rule to recompute': {
valeur: '(rule to replace * 2) * question',
},
'rule to replace': {
valeur: 0,
},
question: {
question: 'Question ?',
},
})
})

it('should fold rules impacted by a [] with multiple contexte rules', () => {
const rawRules = {
root: {
valeur: 'rule to recompute',
Expand Down Expand Up @@ -647,25 +688,6 @@ describe('Constant folding [base]', () => {

it('should not fold rules impacted by a [contexte] with nested mechanisms in the formula', () => {
const rawRules = {
root: {
valeur: {
somme: ['rule to recompute', 'question', 10],
},
contexte: {
constant: 20,
},
},
'rule to recompute': {
valeur: 'constant * 2',
},
question: {
question: 'Question ?',
},
constant: {
valeur: 10,
},
}
expect(constantFoldingWith(rawRules)).toEqual({
root: {
somme: ['rule to recompute', 'question', 10],
contexte: {
Expand All @@ -681,7 +703,8 @@ describe('Constant folding [base]', () => {
constant: {
valeur: 10,
},
})
}
expect(constantFoldingWith(rawRules)).toEqual(rawRules)
})

it('should fold rules impacted by a [contexte] with nested mechanisms in the formula', () => {
Expand Down Expand Up @@ -755,6 +778,34 @@ describe('Constant folding [base]', () => {
})
})

it('should not fold a nullable constant [contexte] rule', () => {
const rawRules = {
root: {
'applicable si': 'présent',
},
'root . présent': {
question: 'Is present?',
'par défaut': 'non',
},
'root . a': {
valeur: 'rule to recompute',
contexte: {
constant: 15,
},
},
'rule to recompute': {
valeur: 'constant * 2',
},
'rule to fold': {
valeur: 'constant * 4',
},
constant: {
valeur: 10,
},
}
expect(constantFoldingWith(rawRules)).toEqual(rawRules)
})

it('replaceAllRefs bug #3', () => {
const rawRules = {
boisson: {
Expand Down

0 comments on commit 7ae9db1

Please sign in to comment.