diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index f206bf8..1168420 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -81,7 +81,8 @@ function initFoldingCtx( // 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. - Object.entries(parsedRules).forEach(([ruleName, ruleNode]) => { + for (const ruleName in parsedRules) { + const ruleNode = parsedRules[ruleName] const reducedAST = reduceAST( (acc: Set, node: ASTNode) => { @@ -123,7 +124,7 @@ function initFoldingCtx( addMapEntry(refs.parents, traversedVar, [ruleName]) }) } - }) + } // All childs of a rule impacted by a contexte rule are also impacted. // @@ -145,7 +146,9 @@ function initFoldingCtx( refs, toKeep, impactedByContexteRules, - params: { isFoldedAttr: foldingParams?.isFoldedAttr ?? 'optimized' }, + params: { + isFoldedAttr: foldingParams?.isFoldedAttr ?? 'optimized', + }, } } @@ -194,6 +197,11 @@ function lexicalSubstitutionOfRefValue( ) { if (constant.explanation.valeur.nodeKind === 'condition') { return constant.explanation.valeur.explanation.alors + } else if ( + constant.explanation.valeur.nodeKind === 'unité' && + constant.explanation.valeur.explanation.nodeKind === 'condition' + ) { + return constant.explanation.valeur.explanation.explanation.alors } else { throw new Error( `[lexicalSubstitutionOfRefValue]: constant node is expected to be a condition. Got ${constant.explanation.valeur.nodeKind} for the rule ${constant.dottedName}`, @@ -223,7 +231,8 @@ function searchAndReplaceConstantValueInParentRefs( const newRule = lexicalSubstitutionOfRefValue(parentRule, rule) if (newRule !== undefined) { ctx.parsedRules[parentName] = newRule - ctx.parsedRules[parentName].rawNode[ctx.params.isFoldedAttr] = true + ctx.parsedRules[parentName].rawNode[ctx.params.isFoldedAttr] = + 'partially' removeInMap(ctx.refs.parents, ruleName, parentName) } } @@ -232,7 +241,11 @@ function searchAndReplaceConstantValueInParentRefs( } function isAlreadyFolded(params: FoldingParams, rule: RuleNode): boolean { - return 'rawNode' in rule && params.isFoldedAttr in rule.rawNode + return ( + 'rawNode' in rule && + params.isFoldedAttr in rule.rawNode && + rule.rawNode[params.isFoldedAttr] === 'fully' + ) } function removeInMap(map: Map>, key: K, val: V) { @@ -327,6 +340,12 @@ function replaceRuleWithEvaluatedNodeValue( rule.explanation.valeur.explanation.nodeKind === 'condition' ) { rule.explanation.valeur.explanation.explanation.alors = explanationThen + } else if ( + rule.explanation.valeur.nodeKind === 'arrondi' && + rule.explanation.valeur.explanation.valeur.nodeKind === 'condition' + ) { + rule.explanation.valeur.explanation.valeur.explanation.alors = + explanationThen } else { throw new Error( `[replaceRuleWithEvaluatedNodeValue]: root rule are expected to be a condition. Got ${rule.explanation.valeur.nodeKind} for the rule ${rule.dottedName}`, @@ -334,27 +353,6 @@ function replaceRuleWithEvaluatedNodeValue( } } -/** - * Subsitutes [parentRuleNode.formule] ref constant from [refs]. - * - * @note It folds child rules in [refs] if possible. - */ -function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: Set) { - if (refs) { - for (const childName of refs) { - const childNode = ctx.parsedRules[childName] - - if ( - childNode && - isFoldable(childNode, ctx.impactedByContexteRules) && - !isAlreadyFolded(ctx.params, childNode) - ) { - fold(ctx, childName, childNode) - } - } - } -} - function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { if ( rule !== undefined && @@ -397,16 +395,10 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { if (ctx.refs.parents.get(ruleName)?.size === 0) { deleteRule(ctx, ruleName) } else { - ctx.parsedRules[ruleName].rawNode[ctx.params.isFoldedAttr] = true + ctx.parsedRules[ruleName].rawNode[ctx.params.isFoldedAttr] = 'fully' } return - } else { - // Try to replace internal refs if possible. - const childs = ctx.refs.childs.get(ruleName) - if (childs?.size > 0) { - replaceAllPossibleChildRefs(ctx, childs) - } } } @@ -425,34 +417,51 @@ export function constantFolding( toKeep?: PredicateOnRule, params?: FoldingParams, ): ParsedRules { + console.time('deepCopy') const parsedRules: ParsedRules = // PERF: could it be avoided? JSON.parse(JSON.stringify(engine.getParsedRules())) + console.timeEnd('deepCopy') + console.time('initFoldingCtx') let ctx = initFoldingCtx(engine, parsedRules, toKeep, params) + console.timeEnd('initFoldingCtx') + + let nbRules = Object.keys(ctx.parsedRules).length + let nbRulesBefore = undefined - for (const ruleName in ctx.parsedRules) { - const ruleNode = ctx.parsedRules[ruleName] + console.time(`fold`) + while (nbRules !== nbRulesBefore) { + for (const ruleName in ctx.parsedRules) { + const ruleNode = ctx.parsedRules[ruleName] - if ( - isFoldable(ruleNode, ctx.impactedByContexteRules) && - !isAlreadyFolded(ctx.params, ruleNode) - ) { - fold(ctx, ruleName, ruleNode) + if ( + isFoldable(ruleNode, ctx.impactedByContexteRules) && + !isAlreadyFolded(ctx.params, ruleNode) + ) { + fold(ctx, ruleName, ruleNode) + } } + nbRulesBefore = nbRules + nbRules = Object.keys(ctx.parsedRules).length } + console.timeEnd(`fold`) if (toKeep) { - ctx.parsedRules = Object.fromEntries( - Object.entries(ctx.parsedRules).filter(([ruleName, ruleNode]) => { - const parents = ctx.refs.parents.get(ruleName) - return ( - !isFoldable(ruleNode, ctx.impactedByContexteRules) || - toKeep([ruleName, ruleNode]) || - parents?.size > 0 - ) - }), - ) + console.time('filter') + for (const ruleName in ctx.parsedRules) { + const ruleNode = ctx.parsedRules[ruleName] + const parents = ctx.refs.parents.get(ruleName) + + if ( + isFoldable(ruleNode, ctx.impactedByContexteRules) && + !toKeep([ruleName, ruleNode]) && + (!parents || parents?.size === 0) + ) { + delete ctx.parsedRules[ruleName] + } + } + console.timeEnd('filter') } return ctx.parsedRules diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 8aed450..e1aaf83 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -66,7 +66,7 @@ describe('Constant folding [base]', () => { ).toStrictEqual({ ruleB: { valeur: 100, - optimized: true, + optimized: 'fully', }, }) }) @@ -85,7 +85,7 @@ describe('Constant folding [base]', () => { ruleA: { titre: 'Rule A', valeur: 30, - optimized: true, + optimized: 'fully', }, }) }) @@ -107,7 +107,7 @@ describe('Constant folding [base]', () => { ruleA: { titre: 'Rule A', valeur: 30, - optimized: true, + optimized: 'fully', }, }) }) @@ -129,7 +129,7 @@ describe('Constant folding [base]', () => { ruleA: { titre: 'Rule A', valeur: '10 * D', - optimized: true, + optimized: 'partially', }, 'ruleA . D': { question: "What's the value of D", @@ -157,7 +157,7 @@ describe('Constant folding [base]', () => { ruleA: { titre: 'Rule A', valeur: '10 * D', - optimized: true, + optimized: 'partially', }, 'ruleA . D': { question: "What's the value of D?", @@ -185,7 +185,7 @@ describe('Constant folding [base]', () => { ruleA: { titre: 'Rule A', valeur: '10 * D', - optimized: true, + optimized: 'partially', }, 'ruleA . D': { question: "What's the value of D?", @@ -208,7 +208,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['A'])).toStrictEqual({ A: { valeur: 70, - optimized: true, + optimized: 'fully', }, }) }) @@ -234,7 +234,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['B'])).toStrictEqual({ B: { valeur: '70 * D', - optimized: true, + optimized: 'partially', }, 'B . D': { question: "What's the value of B . D?", @@ -260,7 +260,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ ruleA: { valeur: 174, - optimized: true, + optimized: 'fully', }, }) }) @@ -291,7 +291,7 @@ describe('Constant folding [base]', () => { }, ruleB: { somme: ['70 * D', 10, 24], - optimized: true, + optimized: 'partially', }, 'ruleB . D': { question: "What's the value of ruleB . D?", @@ -360,7 +360,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['omr'])).toStrictEqual({ omr: { valeur: '0.69068 kgCO2e', - optimized: true, + optimized: 'fully', }, }) }) @@ -384,7 +384,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['biogaz'])).toStrictEqual({ biogaz: { valeur: '(20 * 10) + not foldable', - optimized: true, + optimized: 'partially', }, 'not foldable': { question: 'The user needs to provide a value.', @@ -411,7 +411,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['biogaz'])).toStrictEqual({ biogaz: { valeur: '(10 * 20) + not foldable', - optimized: true, + optimized: 'partially', }, 'not foldable': { question: 'The user needs to provide a value.', @@ -434,7 +434,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['boisson'])).toStrictEqual({ boisson: { valeur: '20 * nombre', - optimized: true, + optimized: 'partially', }, 'boisson . nombre': { 'par défaut': 10, @@ -455,7 +455,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['boisson'])).toStrictEqual({ boisson: { valeur: '20 * nombre', - optimized: true, + optimized: 'partially', }, 'boisson . nombre': { 'par défaut': 10, @@ -476,7 +476,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['boisson'])).toStrictEqual({ boisson: { valeur: '2 % * nombre', - optimized: true, + optimized: 'partially', }, 'boisson . nombre': { 'par défaut': 10, @@ -500,7 +500,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['chocolat chaud'])).toStrictEqual({ 'chocolat chaud': { valeur: '20.3 * nombre', - optimized: true, + optimized: 'partially', }, 'chocolat chaud . nombre': { question: 'Nombre de chocolats chauds par semaine', @@ -525,7 +525,7 @@ describe('Constant folding [base]', () => { ).toStrictEqual({ 'piscine . empreinte': { somme: ['((45 * nombre) * 45) * 45'], - optimized: true, + optimized: 'partially', }, 'piscine . nombre': { question: 'Combien ?', 'par défaut': 2 }, }) @@ -553,7 +553,7 @@ describe('Constant folding [base]', () => { titre: 'Empreinte armoire amortie', valeur: 'armoire . empreinte / (10 * 45)', unité: 'kgCO2e', - optimized: true, + optimized: 'partially', }, 'divers . ameublement . meubles . armoire . empreinte': { question: 'Empreinte?', @@ -620,7 +620,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { valeur: 200, - optimized: true, + optimized: 'fully', }, }) }) @@ -676,8 +676,6 @@ describe('Constant folding [base]', () => { }, constant: { valeur: 10, - // TODO: should be marked as optimized? - // optimized: true, }, }) }) @@ -717,8 +715,6 @@ describe('Constant folding [base]', () => { }, constant: { valeur: 10, - // TODO: should be marked as optimized? - // optimized: true, }, }) }) @@ -744,11 +740,11 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { valeur: 30, - optimized: true, + optimized: 'fully', }, 'rule to fold': { valeur: 40, - optimized: true, + optimized: 'fully', }, }) }) @@ -768,7 +764,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules)).toStrictEqual({ boisson: { valeur: 'tasse de café * 20', - optimized: true, + optimized: 'partially', }, 'boisson . tasse de café': { question: '?', @@ -792,7 +788,7 @@ describe('Constant folding [base]', () => { root: { valeur: '10.99 kgCO2e/semaine', unité: 'kgCO2e/semaine', - optimized: true, + optimized: 'fully', }, }) }) @@ -821,7 +817,7 @@ describe('Constant folding [base]', () => { 'A . B': { 'applicable si': 'présent', valeur: '7 * 10', - optimized: true, + optimized: 'partially', }, 'A . B . présent': { question: 'Is present?', @@ -854,7 +850,7 @@ describe('Constant folding [base]', () => { 'A . B': { 'applicable si': 'présent', valeur: '7 * 10', - optimized: true, + optimized: 'partially', }, 'A . B . présent': { question: 'Is present?', @@ -884,7 +880,7 @@ describe('Constant folding [base]', () => { 'toutes ces conditions': ['unfoldable < 20'], }, valeur: '20 * unfoldable', - optimized: true, + optimized: 'partially', }, 'root . unfoldable': { 'par défaut': 10, @@ -913,7 +909,7 @@ describe('Constant folding [base]', () => { 'toutes ces conditions': ['unfoldable > 20'], }, valeur: '20 * unfoldable', - optimized: true, + optimized: 'partially', }, 'root . unfoldable': { 'par défaut': 10,