From 96faf5908255a1a991b220913d0687a951806394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 3 Jan 2024 09:44:02 +0100 Subject: [PATCH 01/54] fix: try to use the new `contexte` mechanism --- source/optims/constantFolding.ts | 17 +++++++------- test/optims/constantFolding.test.ts | 36 +++++++++++------------------ 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index adb036f..ff5d175 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -31,17 +31,16 @@ type FoldingCtx = { toKeep?: PredicateOnRule params: FoldingParams /** - * The rules that are evaluated with a modified situation (in a [recalcul] mechanism) + * The rules that are evaluated with a modified situation (in a [contexte] mechanism) * and we don't want to be folded. * * @example * ``` * rule: - * recalcul: - * règle: rule2 - * avec: - * rule3: 10 - * rule4: 20 + * valeur: rule2 + * contexte: + * rule3: 10 + * rule4: 20 * ... * ``` * In this case, [rule2] should not be folded. @@ -74,10 +73,10 @@ function initFoldingCtx( reduceAST( (acc: Set, node: ASTNode) => { if ( - node.nodeKind === 'recalcul' && - 'dottedName' in node.explanation.recalculNode + Object.keys(node.rawNode).includes('contexte') && + node.rawNode.valeur !== undefined ) { - recalculRules.add(node.explanation.recalculNode.dottedName) + recalculRules.add(node.rawNode.valeur) } if ( node.nodeKind === 'reference' && diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index d2a2acb..73642ca 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -539,14 +539,12 @@ describe('Constant folding [base]', () => { }) }) - it('should not fold rules used in a [recalcul]', () => { + it('should not fold rules used with a [contexte]', () => { const rawRules = { root: { - recalcul: { - règle: 'rule to recompute', - avec: { - constant: 20, - }, + valeur: 'rule to recompute', + contexte: { + constant: 20, }, }, 'rule to recompute': { @@ -558,11 +556,9 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { - recalcul: { - règle: 'rule to recompute', - avec: { - constant: 20, - }, + valeur: 'rule to recompute', + contexte: { + constant: 20, }, }, 'rule to recompute': { @@ -575,14 +571,12 @@ describe('Constant folding [base]', () => { }) }) - it('should not fold rules used in a [recalcul] but still fold used constant in other rules', () => { + it('should not fold rules used with a [contexte] but still fold used constant in other rules', () => { const rawRules = { root: { - recalcul: { - règle: 'rule to recompute', - avec: { - constant: 20, - }, + valeur: 'rule to recompute', + contexte: { + constant: 20, }, }, 'rule to recompute': { @@ -597,11 +591,9 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { - recalcul: { - règle: 'rule to recompute', - avec: { - constant: 20, - }, + valeur: 'rule to recompute', + contexte: { + constant: 20, }, }, 'rule to recompute': { From b27100fb0f6b81dca298d95240f21cc8cb1815e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 3 Jan 2024 09:44:55 +0100 Subject: [PATCH 02/54] fix: set parent to each rule in rules set --- test/getRawRules.test.ts | 3 +++ test/optims/constantFolding.test.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/test/getRawRules.test.ts b/test/getRawRules.test.ts index 2f6996f..647baef 100644 --- a/test/getRawRules.test.ts +++ b/test/getRawRules.test.ts @@ -36,6 +36,7 @@ describe('getRawRules', () => { titre: 'Rule A', formule: 'B . C * 3', }, + 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -51,6 +52,7 @@ describe('getRawRules', () => { titre: 'Rule A', formule: 'B . C * 3', }, + 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -60,6 +62,7 @@ describe('getRawRules', () => { titre: 'Rule A', formule: 'B . C * 3', }, + 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 73642ca..4cd01fa 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -27,6 +27,7 @@ describe('Constant folding [meta]', () => { titre: 'Rule A', formule: 'B . C * D', }, + 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -70,6 +71,7 @@ describe('Constant folding [base]', () => { titre: 'Rule A', formule: 'B . C * 3', }, + 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -88,6 +90,7 @@ describe('Constant folding [base]', () => { titre: 'Rule A', formule: 'B . C * D', }, + 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -112,6 +115,7 @@ describe('Constant folding [base]', () => { 'ruleA . D': { question: "What's the value of D", }, + 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, From bbebc7609ad87c540dff35f65cdbcc820d7a70a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 3 Jan 2024 13:34:56 +0100 Subject: [PATCH 03/54] fix: add traversedVariables option --- source/optims/constantFolding.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index ff5d175..ef25e0d 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -346,6 +346,7 @@ function tryToFoldRule( return } + ctx.engine.cache.traversedVariablesStack = [] const { nodeValue, missingVariables, traversedVariables, unit } = ctx.engine.evaluateNode(rule) From 185aae87b58a3725a6928dcb55d0439ea5e16582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Wed, 3 Jan 2024 13:43:30 +0100 Subject: [PATCH 04/54] fix: set parent to each rule in rules set again --- test/optims/constantFolding.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 4cd01fa..b3b7780 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -143,6 +143,7 @@ describe('Constant folding [base]', () => { 'ruleA . D': { question: "What's the value of D?", }, + 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -164,6 +165,7 @@ describe('Constant folding [base]', () => { titre: 'Rule A', formule: 'B . C * D', }, + 'ruleA . B': null, ruleB: { formule: 'ruleA . B . C * 3', }, @@ -240,6 +242,7 @@ describe('Constant folding [base]', () => { ruleB: { somme: ['A . B * 2', 10, 12 * 2], }, + A: null, 'A . B': { formule: 'C * 10', }, @@ -267,6 +270,7 @@ describe('Constant folding [base]', () => { 'ruleB . D': { question: "What's the value of ruleB . D?", }, + A: null, 'A . B': { formule: 'C * 10', }, @@ -363,6 +367,7 @@ describe('Constant folding [base]', () => { "biogaz . facteur d'émission": { valeur: 20, }, + gaz: null, "gaz . facteur d'émission": { valeur: 10, }, @@ -389,6 +394,7 @@ describe('Constant folding [base]', () => { "biogaz . facteur d'émission": { valeur: 20, }, + gaz: null, "gaz . facteur d'émission": { valeur: 10, }, @@ -515,6 +521,10 @@ describe('Constant folding [base]', () => { }) it('should work with parentheses inside [formule]', () => { const rawRules = { + divers: null, + 'divers . ameublement': null, + 'divers . ameublement . meubles': null, + 'divers . ameublement . meubles . armoire': null, 'divers . ameublement . meubles . armoire . empreinte amortie': { titre: 'Empreinte armoire amortie', formule: 'armoire . empreinte / (durée * coefficient préservation)', From 1ddd2b4036bd72cb549baea9cc52e5534990bfb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Fri, 5 Jan 2024 18:34:28 +0100 Subject: [PATCH 05/54] fix: `nom` is not an attribute anymore --- source/compilation/resolveImports.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/source/compilation/resolveImports.ts b/source/compilation/resolveImports.ts index 3e0da44..4bc9227 100644 --- a/source/compilation/resolveImports.ts +++ b/source/compilation/resolveImports.ts @@ -162,21 +162,6 @@ ${importedRule.description}` } } -/** - * @throws {Error} If the `nom` attribute is different from the `ruleNameToCheck`. - */ -function removeRawNodeNom( - rawNode: Rule, - ruleNameToCheck: string, -): Omit { - const { nom, ...rest } = rawNode - if (nom !== ruleNameToCheck) - throw Error( - `Imported rule's publicode raw node "nom" attribute is different from the resolveImport script ruleName. Please investigate`, - ) - return rest -} - function appearsMoreThanOnce( rulesToImport: RuleToImport[], ruleName: RuleName, @@ -252,10 +237,7 @@ export function resolveImports( utils .ruleParents(ruleName) .forEach((rule) => neededNamespaces.add(`${namespace} . ${rule}`)) - return [ - `${namespace} . ${ruleName}`, - removeRawNodeNom(ruleWithUpdatedDescription, ruleName), - ] + return [`${namespace} . ${ruleName}`, ruleWithUpdatedDescription] } const ruleWithOverridenAttributes = { ...rule.rawNode, ...attrs } From 074ed79885e6227a2a76da6406aacd6c62dac48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Fri, 5 Jan 2024 19:27:07 +0100 Subject: [PATCH 06/54] feat: try to set a list of `contextRules` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @EmileRolley j'ai tenté un truc mais le souci c'est qu'on n'a plus dans la règle qu'on ne doit pas optimiser l'info du recalcul/context mais seulement via le parent, règle par laquelle on passe "trop" tard --- source/optims/constantFolding.ts | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index ef25e0d..4bde218 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -45,7 +45,7 @@ type FoldingCtx = { * ``` * In this case, [rule2] should not be folded. */ - recalculRules: Set + contextRules: Set } function addMapEntry(map: RefMap, key: RuleName, values: RuleName[]) { @@ -66,17 +66,14 @@ function initFoldingCtx( parents: new Map(), childs: new Map(), } - const recalculRules = new Set() + const contextRules = new Set() Object.entries(parsedRules).forEach(([ruleName, ruleNode]) => { const reducedAST = reduceAST( (acc: Set, node: ASTNode) => { - if ( - Object.keys(node.rawNode).includes('contexte') && - node.rawNode.valeur !== undefined - ) { - recalculRules.add(node.rawNode.valeur) + if (Object.keys(node.rawNode).includes('contexte')) { + contextRules.add(node.rawNode.valeur) } if ( node.nodeKind === 'reference' && @@ -106,22 +103,24 @@ function initFoldingCtx( parsedRules, refs, toKeep, - recalculRules, + contextRules, params: { isFoldedAttr: foldingParams?.isFoldedAttr ?? 'optimized' }, } } function isFoldable( rule: RuleNode | undefined, - recalculRules: Set, + contextRules: Set, ): boolean { if (!rule) { return false } const rawNode = rule.rawNode + + console.log(rule.dottedName, contextRules, contextRules.has(rule.dottedName)) return !( - recalculRules.has(rule.dottedName) || + contextRules.has(rule.dottedName) || 'question' in rawNode || // NOTE(@EmileRolley): I assume that a rule can have a [par défaut] attribute without a [question] one. // The behavior could be specified. @@ -239,7 +238,7 @@ function searchAndReplaceConstantValueInParentRefs( for (const parentName of refs) { let parentRule = ctx.parsedRules[parentName] - if (isFoldable(parentRule, ctx.recalculRules)) { + if (isFoldable(parentRule, ctx.contextRules)) { const newRule = lexicalSubstitutionOfRefValue(parentRule, rule) if (newRule !== undefined) { parentRule = newRule @@ -267,7 +266,7 @@ function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: RuleName[]): void { if ( childNode && - isFoldable(childNode, ctx.recalculRules) && + isFoldable(childNode, ctx.contextRules) && !isAlreadyFolded(ctx.params, childNode) ) { tryToFoldRule(ctx, childName, childNode) @@ -297,7 +296,7 @@ function deleteRule(ctx: FoldingCtx, dottedName: RuleName): void { const ruleNode = ctx.parsedRules[dottedName] if ( (ctx.toKeep === undefined || !ctx.toKeep([dottedName, ruleNode])) && - isFoldable(ruleNode, ctx.recalculRules) + isFoldable(ruleNode, ctx.contextRules) ) { removeRuleFromRefs(ctx.refs.parents, dottedName) removeRuleFromRefs(ctx.refs.childs, dottedName) @@ -328,7 +327,7 @@ function tryToFoldRule( ): void { if ( rule !== undefined && - (!isFoldable(rule, ctx.recalculRules) || + (!isFoldable(rule, ctx.contextRules) || isAlreadyFolded(ctx.params, rule) || !(ruleName in ctx.parsedRules)) ) { @@ -440,7 +439,7 @@ export function constantFolding( Object.entries(ctx.parsedRules).forEach(([ruleName, ruleNode]) => { if ( - isFoldable(ruleNode, ctx.recalculRules) && + isFoldable(ruleNode, ctx.contextRules) && !isAlreadyFolded(ctx.params, ruleNode) ) { tryToFoldRule(ctx, ruleName, ruleNode) @@ -452,7 +451,7 @@ export function constantFolding( Object.entries(ctx.parsedRules).filter(([ruleName, ruleNode]) => { const parents = ctx.refs.parents.get(ruleName) return ( - !isFoldable(ruleNode, ctx.recalculRules) || + !isFoldable(ruleNode, ctx.contextRules) || toKeep([ruleName, ruleNode]) || parents?.length > 0 ) From 964f58a854d7f0065468ffa6557e4825ba7d4c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Fri, 5 Jan 2024 19:38:29 +0100 Subject: [PATCH 07/54] fix: an empty rule length is now 0 --- source/optims/constantFolding.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 4bde218..a869d76 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -131,8 +131,7 @@ function isFoldable( } function isEmptyRule(rule: RuleNode): boolean { - // There is always a 'nom' attribute. - return Object.keys(rule.rawNode).length <= 1 + return Object.keys(rule.rawNode).length === 0 } function formatPublicodesUnit(unit?: Unit): string { From d35adfd333cdf4aca3ffcc5add0e7ceab5143625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Fri, 5 Jan 2024 20:04:41 +0100 Subject: [PATCH 08/54] fix: add ruleName that includes context on contextRules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit J'ai raconté n'importe quoi dans mon dernier commit ! 😶 --- source/optims/constantFolding.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index a869d76..e75b29d 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -72,7 +72,11 @@ function initFoldingCtx( const reducedAST = reduceAST( (acc: Set, node: ASTNode) => { - if (Object.keys(node.rawNode).includes('contexte')) { + if ( + node.rawNode != null && + Object.keys(node.rawNode).includes('contexte') + ) { + contextRules.add(ruleName) contextRules.add(node.rawNode.valeur) } if ( @@ -86,6 +90,7 @@ function initFoldingCtx( new Set(), ruleNode.explanation.valeur, ) ?? new Set() + const traversedVariables: RuleName[] = Array.from(reducedAST).filter( (name) => !name.endsWith(' . $SITUATION'), ) @@ -118,7 +123,6 @@ function isFoldable( const rawNode = rule.rawNode - console.log(rule.dottedName, contextRules, contextRules.has(rule.dottedName)) return !( contextRules.has(rule.dottedName) || 'question' in rawNode || From 3c3ea5f4fde2c48bc5b243020977f597c8f5fdb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Fri, 5 Jan 2024 20:16:09 +0100 Subject: [PATCH 09/54] feat: add rules into context to contextRules Pas sur que ce soit utile mais j'ai toujours une erreur avec l'optim alors que tous les tests passent ici :/ --- source/optims/constantFolding.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index e75b29d..615b2ce 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -78,6 +78,9 @@ function initFoldingCtx( ) { contextRules.add(ruleName) contextRules.add(node.rawNode.valeur) + Object.keys(node.rawNode.contexte).map((dottedNameInContext) => + contextRules.add(dottedNameInContext), + ) } if ( node.nodeKind === 'reference' && From 46e976a895041fe9ec8fec40187a7ae237e2e813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= Date: Fri, 5 Jan 2024 20:22:55 +0100 Subject: [PATCH 10/54] Revert "feat: add rules into context to contextRules" This reverts commit 6447efc917536bbab1200ca50ab579066cef8b15. --- source/optims/constantFolding.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 615b2ce..e75b29d 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -78,9 +78,6 @@ function initFoldingCtx( ) { contextRules.add(ruleName) contextRules.add(node.rawNode.valeur) - Object.keys(node.rawNode.contexte).map((dottedNameInContext) => - contextRules.add(dottedNameInContext), - ) } if ( node.nodeKind === 'reference' && From 883cbf0146ff82ee71ed50076949e54953bbee68 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 8 Jan 2024 13:00:40 +0100 Subject: [PATCH 11/54] fix(optim): retrieve all rules impacted by a contexte mechanism --- source/commons.ts | 2 +- source/optims/constantFolding.ts | 19 +++++++++++---- test/optims/constantFolding.test.ts | 36 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/source/commons.ts b/source/commons.ts index 33b4157..11de42a 100644 --- a/source/commons.ts +++ b/source/commons.ts @@ -115,7 +115,7 @@ export const disabledLogger: Logger = { * * @returns The references. */ -export function getAllRefsInNode(node: RuleNode): RuleName[] { +export function getAllRefsInNode(node: ASTNode): RuleName[] { return reduceAST( (refs: RuleName[], node: ASTNode) => { if (node === undefined) { diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index e75b29d..1dc7cbe 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -1,6 +1,7 @@ import Engine, { reduceAST, ParsedRules, parseExpression } from 'publicodes' import type { RuleNode, ASTNode, Unit } from 'publicodes' import { + getAllRefsInNode, RuleName, serializeParsedExprAST, substituteInParsedExpr, @@ -72,12 +73,20 @@ function initFoldingCtx( const reducedAST = reduceAST( (acc: Set, node: ASTNode) => { - if ( - node.rawNode != null && - Object.keys(node.rawNode).includes('contexte') - ) { + if (node.nodeKind === 'contexte') { + // Find all rule references impacted by the contexte in the rule node + const impactedRules = getAllRefsInNode(node).filter((ref) => { + return ( + ref !== ruleName && + !ref.endsWith(' . $SITUATION') && + !node.explanation.contexte.find( + ([contexteRef, _]) => contexteRef.dottedName === ref, + ) + ) + }) + + impactedRules.forEach((rule) => contextRules.add(rule)) contextRules.add(ruleName) - contextRules.add(node.rawNode.valeur) } if ( node.nodeKind === 'reference' && diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index b3b7780..0fdf5e2 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -585,6 +585,42 @@ describe('Constant folding [base]', () => { }) }) + it('should not fold rules used with a [contexte] impacting a complex formula', () => { + const rawRules = { + root: { + formule: { + somme: ['rule to recompute', 10], + }, + contexte: { + constant: 20, + }, + }, + 'rule to recompute': { + formule: 'constant * 2', + }, + constant: { + valeur: 10, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + root: { + formule: { + somme: ['rule to recompute', 10], + }, + contexte: { + constant: 20, + }, + }, + 'rule to recompute': { + formule: 'constant * 2', + }, + constant: { + valeur: 10, + optimized: true, + }, + }) + }) + it('should not fold rules used with a [contexte] but still fold used constant in other rules', () => { const rawRules = { root: { From 2aad8c15a963e4b3dd89fd882e98296c68a8cf97 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 8 Jan 2024 16:11:19 +0100 Subject: [PATCH 12/54] wip: manage new contexte --- source/optims/constantFolding.ts | 84 ++++++++++++++++++------- test/optims/constantFolding.test.ts | 97 +++++++++++++++++++++++++++-- 2 files changed, 154 insertions(+), 27 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 1dc7cbe..c01e597 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -1,4 +1,9 @@ -import Engine, { reduceAST, ParsedRules, parseExpression } from 'publicodes' +import Engine, { + reduceAST, + ParsedRules, + parseExpression, + utils, +} from 'publicodes' import type { RuleNode, ASTNode, Unit } from 'publicodes' import { getAllRefsInNode, @@ -46,7 +51,7 @@ type FoldingCtx = { * ``` * In this case, [rule2] should not be folded. */ - contextRules: Set + impactedByContexteRules: Set } function addMapEntry(map: RefMap, key: RuleName, values: RuleName[]) { @@ -67,7 +72,7 @@ function initFoldingCtx( parents: new Map(), childs: new Map(), } - const contextRules = new Set() + const impactedByContexteRules = new Set() Object.entries(parsedRules).forEach(([ruleName, ruleNode]) => { const reducedAST = @@ -75,18 +80,14 @@ function initFoldingCtx( (acc: Set, node: ASTNode) => { if (node.nodeKind === 'contexte') { // Find all rule references impacted by the contexte in the rule node - const impactedRules = getAllRefsInNode(node).filter((ref) => { - return ( - ref !== ruleName && - !ref.endsWith(' . $SITUATION') && - !node.explanation.contexte.find( - ([contexteRef, _]) => contexteRef.dottedName === ref, - ) - ) - }) - - impactedRules.forEach((rule) => contextRules.add(rule)) - contextRules.add(ruleName) + const impactedRules = getAllRefsInNodeImpactedByContexte( + ruleName, + node, + node.explanation.contexte.map(([ref, _]) => ref.dottedName), + ) + + impactedRules.forEach((rule) => impactedByContexteRules.add(rule)) + impactedByContexteRules.add(ruleName) } if ( node.nodeKind === 'reference' && @@ -112,16 +113,55 @@ function initFoldingCtx( } }) + // All childs of a rule impacted by a contexte rule are also impacted. + // + // NOTE(@EmileRolley): contexte rule will be added in the contextRules set. + // Therefore, they won't be marked as folded. It's a wanted behavior? Not sure. + for (const ruleName of impactedByContexteRules) { + getAllChilds(ruleName, refs.childs).forEach((rule) => + impactedByContexteRules.add(rule), + ) + } + return { engine, parsedRules, refs, toKeep, - contextRules, + impactedByContexteRules, params: { isFoldedAttr: foldingParams?.isFoldedAttr ?? 'optimized' }, } } +function getAllRefsInNodeImpactedByContexte( + ruleName: RuleName, + node: ASTNode, + contexteRefs: RuleName[], +): RuleName[] { + const impactedRules = getAllRefsInNode(node).filter((ref) => { + return ( + ref !== ruleName && + !ref.endsWith(' . $SITUATION') && + !contexteRefs.includes(ref) + ) + }) + + return impactedRules +} + +function getAllChilds(ruleName: RuleName, childs: RefMap): RuleName[] { + const allChilds = [] + + for (const child of childs.get(ruleName) ?? []) { + if (!allChilds.includes(child)) { + allChilds.push(child) + allChilds.push(...getAllChilds(child, childs)) + } + } + + return allChilds +} + function isFoldable( rule: RuleNode | undefined, contextRules: Set, @@ -250,7 +290,7 @@ function searchAndReplaceConstantValueInParentRefs( for (const parentName of refs) { let parentRule = ctx.parsedRules[parentName] - if (isFoldable(parentRule, ctx.contextRules)) { + if (isFoldable(parentRule, ctx.impactedByContexteRules)) { const newRule = lexicalSubstitutionOfRefValue(parentRule, rule) if (newRule !== undefined) { parentRule = newRule @@ -278,7 +318,7 @@ function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: RuleName[]): void { if ( childNode && - isFoldable(childNode, ctx.contextRules) && + isFoldable(childNode, ctx.impactedByContexteRules) && !isAlreadyFolded(ctx.params, childNode) ) { tryToFoldRule(ctx, childName, childNode) @@ -308,7 +348,7 @@ function deleteRule(ctx: FoldingCtx, dottedName: RuleName): void { const ruleNode = ctx.parsedRules[dottedName] if ( (ctx.toKeep === undefined || !ctx.toKeep([dottedName, ruleNode])) && - isFoldable(ruleNode, ctx.contextRules) + isFoldable(ruleNode, ctx.impactedByContexteRules) ) { removeRuleFromRefs(ctx.refs.parents, dottedName) removeRuleFromRefs(ctx.refs.childs, dottedName) @@ -339,7 +379,7 @@ function tryToFoldRule( ): void { if ( rule !== undefined && - (!isFoldable(rule, ctx.contextRules) || + (!isFoldable(rule, ctx.impactedByContexteRules) || isAlreadyFolded(ctx.params, rule) || !(ruleName in ctx.parsedRules)) ) { @@ -451,7 +491,7 @@ export function constantFolding( Object.entries(ctx.parsedRules).forEach(([ruleName, ruleNode]) => { if ( - isFoldable(ruleNode, ctx.contextRules) && + isFoldable(ruleNode, ctx.impactedByContexteRules) && !isAlreadyFolded(ctx.params, ruleNode) ) { tryToFoldRule(ctx, ruleName, ruleNode) @@ -463,7 +503,7 @@ export function constantFolding( Object.entries(ctx.parsedRules).filter(([ruleName, ruleNode]) => { const parents = ctx.refs.parents.get(ruleName) return ( - !isFoldable(ruleNode, ctx.contextRules) || + !isFoldable(ruleNode, ctx.impactedByContexteRules) || toKeep([ruleName, ruleNode]) || parents?.length > 0 ) diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 0fdf5e2..58db94e 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -553,7 +553,7 @@ describe('Constant folding [base]', () => { }) }) - it('should not fold rules used with a [contexte]', () => { + it('should not fold rules impacted by a [contexte]', () => { const rawRules = { root: { valeur: 'rule to recompute', @@ -580,12 +580,97 @@ describe('Constant folding [base]', () => { }, constant: { valeur: 10, - optimized: true, + // TODO: should be marked as optimized? + // optimized: true, }, }) }) - it('should not fold rules used with a [contexte] impacting a complex formula', () => { + it('should not fold nested rules impacted by a [contexte]', () => { + const rawRules = { + root: { + valeur: 'rule to recompute', + contexte: { + constant: 20, + }, + }, + 'rule to recompute': { + formule: 'rule to recompute . nested * 2', + }, + 'rule to recompute . nested': { + formule: 'constant * 4', + }, + constant: { + valeur: 10, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + root: { + valeur: 'rule to recompute', + contexte: { + constant: 20, + }, + }, + 'rule to recompute': { + formule: 'rule to recompute . nested * 2', + }, + 'rule to recompute . nested': { + formule: 'constant * 4', + }, + constant: { + valeur: 10, + // TODO: should be marked as optimized? + // optimized: true, + }, + }) + }) + + it('should not fold nested rules (2 deep) impacted by a [contexte]', () => { + const rawRules = { + root: { + valeur: 'rule to recompute', + contexte: { + constant: 20, + }, + }, + 'rule to recompute': { + formule: 'nested 1 * 2', + }, + 'rule to recompute . nested 1': { + formule: 'nested 2 * 4', + }, + 'rule to recompute . nested 2': { + formule: 'constant * 4', + }, + constant: { + valeur: 10, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + root: { + valeur: 'rule to recompute', + contexte: { + constant: 20, + }, + }, + 'rule to recompute': { + formule: 'nested 1 * 2', + }, + 'rule to recompute . nested 1': { + formule: 'nested 2 * 4', + }, + 'rule to recompute . nested 2': { + formule: 'constant * 4', + }, + constant: { + valeur: 10, + // TODO: should be marked as optimized? + // optimized: true, + }, + }) + }) + + it('should not fold rules impacted by a [contexte] with nested mechanisms in the formula', () => { const rawRules = { root: { formule: { @@ -616,7 +701,8 @@ describe('Constant folding [base]', () => { }, constant: { valeur: 10, - optimized: true, + // TODO: should be marked as optimized? + // optimized: true, }, }) }) @@ -655,7 +741,8 @@ describe('Constant folding [base]', () => { }, constant: { valeur: 10, - optimized: true, + // TODO: should be marked as optimized? + // optimized: true, }, }) }) From e446b373271344c39512943db39174f328dfa1bf Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 8 Jan 2024 17:04:45 +0100 Subject: [PATCH 13/54] fix(optim): over-fold rules that could be impacted by a contexte --- source/commons.ts | 2 +- source/compilation/resolveImports.ts | 2 +- source/optims/constantFolding.ts | 39 ++++++++++++---------------- test/optims/constantFolding.test.ts | 6 +++++ 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/source/commons.ts b/source/commons.ts index 11de42a..4f144bc 100644 --- a/source/commons.ts +++ b/source/commons.ts @@ -67,7 +67,7 @@ export type ImportMacro = { /** * Represents a non-parsed NGC rule. */ -export type RawRule = Omit & ImportMacro +export type RawRule = Omit | ImportMacro /** * Represents a non-parsed NGC model. diff --git a/source/compilation/resolveImports.ts b/source/compilation/resolveImports.ts index 4bc9227..b51aa50 100644 --- a/source/compilation/resolveImports.ts +++ b/source/compilation/resolveImports.ts @@ -203,7 +203,7 @@ export function resolveImports( let neededNamespaces = new Set() const resolvedRules = Object.entries(rules).reduce((acc, [name, value]) => { if (name === IMPORT_KEYWORD) { - const importMacro: ImportMacro = value + const importMacro = value as ImportMacro const engine = getEngine(filePath, importMacro, verbose) const rulesToImport: RuleToImport[] = importMacro['les règles']?.map(getRuleToImportInfos) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index c01e597..5f6cfcc 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -1,9 +1,4 @@ -import Engine, { - reduceAST, - ParsedRules, - parseExpression, - utils, -} from 'publicodes' +import Engine, { reduceAST, ParsedRules, parseExpression } from 'publicodes' import type { RuleNode, ASTNode, Unit } from 'publicodes' import { getAllRefsInNode, @@ -49,7 +44,14 @@ type FoldingCtx = { * rule4: 20 * ... * ``` - * In this case, [rule2] should not be folded. + * In this case, [rule2] should not be folded (and all its dependencies + * should not be folded!). + * + * TODO(@EmileRolley): currently, all childs of a rule with a [contexte] + * mechanism are not folded. However, it could be smarter to keep track of + * each contexte rules and fold the child rules that are not impacted by the + * contexte. For now we choose to keep it simple and to over-fold instead of + * taking the risk to alter the result. */ impactedByContexteRules: Set } @@ -117,10 +119,14 @@ function initFoldingCtx( // // NOTE(@EmileRolley): contexte rule will be added in the contextRules set. // Therefore, they won't be marked as folded. It's a wanted behavior? Not sure. + // + // WARN(@EmileRolley): the [impactedByContexteRules] is updated while + // iterating it's convenient but the semantics may vary depending on the + // javascript runtime used. for (const ruleName of impactedByContexteRules) { - getAllChilds(ruleName, refs.childs).forEach((rule) => - impactedByContexteRules.add(rule), - ) + refs.childs + .get(ruleName) + ?.forEach((rule) => impactedByContexteRules.add(rule)) } return { @@ -149,19 +155,6 @@ function getAllRefsInNodeImpactedByContexte( return impactedRules } -function getAllChilds(ruleName: RuleName, childs: RefMap): RuleName[] { - const allChilds = [] - - for (const child of childs.get(ruleName) ?? []) { - if (!allChilds.includes(child)) { - allChilds.push(child) - allChilds.push(...getAllChilds(child, childs)) - } - } - - return allChilds -} - function isFoldable( rule: RuleNode | undefined, contextRules: Set, diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 58db94e..82abbdd 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -640,6 +640,9 @@ describe('Constant folding [base]', () => { formule: 'nested 2 * 4', }, 'rule to recompute . nested 2': { + formule: 'nested 3 * 4', + }, + 'rule to recompute . nested 3': { formule: 'constant * 4', }, constant: { @@ -660,6 +663,9 @@ describe('Constant folding [base]', () => { formule: 'nested 2 * 4', }, 'rule to recompute . nested 2': { + formule: 'nested 3 * 4', + }, + 'rule to recompute . nested 3': { formule: 'constant * 4', }, constant: { From d38cda2f95372c8d9f28f3a212fd07ad848792ec Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Tue, 9 Jan 2024 16:31:57 +0100 Subject: [PATCH 14/54] refactor(optim): remove the use of traversedVariables only use refs.childs --- source/optims/constantFolding.ts | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 5f6cfcc..1a0bedb 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -390,17 +390,9 @@ function tryToFoldRule( return } - ctx.engine.cache.traversedVariablesStack = [] - const { nodeValue, missingVariables, traversedVariables, unit } = - ctx.engine.evaluateNode(rule) + const { nodeValue, missingVariables, unit } = ctx.engine.evaluateNode(rule) - const traversedVariablesWithoutSelf = traversedVariables.filter( - (dottedName) => dottedName !== ruleName, - ) - - // NOTE(@EmileRolley): we need to evaluate due to possible standalone rule [formule] - // parsed as a [valeur]. - if ('valeur' in rule.rawNode && traversedVariablesWithoutSelf?.length > 0) { + if ('valeur' in rule.rawNode) { rule.rawNode.formule = rule.rawNode.valeur delete rule.rawNode.valeur } @@ -429,16 +421,7 @@ function tryToFoldRule( // it from the [refs]. const childs = ctx.refs.childs.get(ruleName) ?? [] - updateRefCounting( - ctx, - ruleName, - // NOTE(@EmileRolley): for some reason, the [traversedVariables] are not always - // depencies of the rule. Consequently, we need to keep only the ones that are - // in the [childs] list in order to avoid removing rules that are not dependencies. - traversedVariablesWithoutSelf?.filter((v: RuleName) => - childs.includes(v), - ) ?? [], - ) + updateRefCounting(ctx, ruleName, childs) delete ctx.parsedRules[ruleName].rawNode.formule } From 444e738d103d82d15ca0ca2cd39449b451344a94 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Tue, 19 Dec 2023 11:37:13 +0100 Subject: [PATCH 15/54] perf(optim): replace isInParsedRules with inline 'ruleName in ctx.parsedRules' --- source/optims/constantFolding.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 1a0bedb..39771e3 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -76,6 +76,10 @@ function initFoldingCtx( } const impactedByContexteRules = new Set() + // 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. Object.entries(parsedRules).forEach(([ruleName, ruleNode]) => { const reducedAST = reduceAST( From 2c60468d1e6d8e81b6e8887f482a3121174dd17d Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Tue, 19 Dec 2023 11:48:19 +0100 Subject: [PATCH 16/54] perf(optim): simplify the removeRuleFromRefs --- source/optims/constantFolding.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 39771e3..aeddc36 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -324,21 +324,19 @@ function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: RuleName[]): void { } } -export function removeInMap( - map: Map, - key: K, - val: V, -): Map { - return map.set( - key, - (map.get(key) ?? []).filter((v) => v !== val), - ) +function removeInMap(map: Map, key: K, val: V) { + if (map.has(key)) { + map.set( + key, + map.get(key).filter((v) => v !== val), + ) + } } function removeRuleFromRefs(ref: RefMap, ruleName: RuleName) { - ref.forEach((_, rule) => { + for (const rule of ref.keys()) { removeInMap(ref, rule, ruleName) - }) + } } function deleteRule(ctx: FoldingCtx, dottedName: RuleName): void { @@ -463,11 +461,15 @@ export function constantFolding( toKeep?: PredicateOnRule, params?: FoldingParams, ): ParsedRules { + console.time('copy') const parsedRules: ParsedRules = // PERF: could it be avoided? JSON.parse(JSON.stringify(engine.getParsedRules())) + console.timeEnd('copy') + console.time('initFoldingCtx') let ctx: FoldingCtx = initFoldingCtx(engine, parsedRules, toKeep, params) + console.timeEnd('initFoldingCtx') Object.entries(ctx.parsedRules).forEach(([ruleName, ruleNode]) => { if ( From 3c8ea5e915f3d2c335f8dab46727aed2f7b04abe Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Tue, 19 Dec 2023 12:01:56 +0100 Subject: [PATCH 17/54] perf(optim): don't return ctx as it's already mutated in place --- source/optims/constantFolding.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index aeddc36..e10ec64 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -308,7 +308,7 @@ function isAlreadyFolded(params: FoldingParams, rule: RuleNode): boolean { * * @note It folds child rules in [refs] if possible. */ -function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: RuleName[]): void { +function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: RuleName[]) { if (refs) { for (const childName of refs) { const childNode = ctx.parsedRules[childName] @@ -358,8 +358,8 @@ function updateRefCounting( ctx: FoldingCtx, parentRuleName: RuleName, ruleNamesToUpdate: RuleName[], -): void { - ruleNamesToUpdate.forEach((ruleNameToUpdate) => { +) { + for (const ruleNameToUpdate of ruleNamesToUpdate) { removeInMap(ctx.refs.parents, ruleNameToUpdate, parentRuleName) if (ctx.refs.parents.get(ruleNameToUpdate)?.length === 0) { deleteRule(ctx, ruleNameToUpdate) @@ -468,17 +468,20 @@ export function constantFolding( console.timeEnd('copy') console.time('initFoldingCtx') - let ctx: FoldingCtx = initFoldingCtx(engine, parsedRules, toKeep, params) + let ctx: FoldingCtx = { + engine, + parsedRules, + toKeep, + refs: getRefs(parsedRules), + params: params?.isFoldedAttr ? params : { isFoldedAttr: 'optimized' }, + } console.timeEnd('initFoldingCtx') - Object.entries(ctx.parsedRules).forEach(([ruleName, ruleNode]) => { - if ( - isFoldable(ruleNode, ctx.impactedByContexteRules) && - !isAlreadyFolded(ctx.params, ruleNode) - ) { + for (const [ruleName, ruleNode] of Object.entries(ctx.parsedRules)) { + if (isFoldable(ruleNode) && !isAlreadyFolded(ctx.params, ruleNode)) { tryToFoldRule(ctx, ruleName, ruleNode) } - }) + } if (toKeep) { ctx.parsedRules = Object.fromEntries( From 4447437a39556aaf15185c9f2ed006db3caf04a9 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Tue, 19 Dec 2023 13:27:08 +0100 Subject: [PATCH 18/54] refactor(optim): use serializeEvaluation from publicodes instead of the local one --- source/optims/constantFolding.ts | 59 ++++++++++++----------------- test/getRawRules.test.ts | 2 + test/optims/constantFolding.test.ts | 2 +- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index e10ec64..3f1039f 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -1,4 +1,9 @@ -import Engine, { reduceAST, ParsedRules, parseExpression } from 'publicodes' +import Engine, { + reduceAST, + ParsedRules, + parseExpression, + serializeEvaluation, +} from 'publicodes' import type { RuleNode, ASTNode, Unit } from 'publicodes' import { getAllRefsInNode, @@ -172,8 +177,8 @@ function isFoldable( return !( contextRules.has(rule.dottedName) || 'question' in rawNode || - // NOTE(@EmileRolley): I assume that a rule can have a [par défaut] attribute without a [question] one. - // The behavior could be specified. + // NOTE(@EmileRolley): I assume that a rule can have a [par défaut] + // attribute without a [question] one. The behavior could be specified. 'par défaut' in rawNode || 'applicable si' in rawNode || 'non applicable si' in rawNode @@ -184,26 +189,6 @@ function isEmptyRule(rule: RuleNode): boolean { return Object.keys(rule.rawNode).length === 0 } -function formatPublicodesUnit(unit?: Unit): string { - if ( - unit !== undefined && - unit.numerators.length === 1 && - unit.numerators[0] === '%' - ) { - return '%' - } - return '' -} - -// Replaces boolean values by their string representation in French. -function formatToPulicodesValue(value: any, unit?: Unit) { - if (typeof value === 'boolean') { - return value ? 'oui' : 'non' - } - - return value + formatPublicodesUnit(unit) -} - function replaceAllRefs( str: string, refName: string, @@ -225,6 +210,10 @@ function lexicalSubstitutionOfRefValue( ): RuleNode | undefined { // Retrieves the name form used in the rule. For exemple, the rule 'root . a // . b' could have the name 'b', 'a . b' or 'root . a . b'. + // const substituteAST = transformAST((node, transform) => { + // // if + // }) + const refName = reduceAST( (_, node: ASTNode) => { if ( @@ -238,8 +227,15 @@ function lexicalSubstitutionOfRefValue( parent, ) - const constValue = formatToPulicodesValue(constant.rawNode.valeur) + const constValue = constant.rawNode.valeur + // NOTE: here we directly replace the [rawNode] as it's what we get back with [getRawNodes] + // at the end. + // Instead, we could transform the complete parsed rule and serialize it at the end. + // + // If I continue to transform directly the [rawNode] then I can use directly the + // rules given to the engine and no need to make a deep copy therefore. We simply + // need to add the dottedName info in the rawRule. if ('formule' in parent.rawNode) { if (typeof parent.rawNode.formule === 'string') { const newFormule = replaceAllRefs( @@ -399,19 +395,12 @@ function tryToFoldRule( delete rule.rawNode.valeur } - const missingVariablesNames = Object.keys(missingVariables) + const missingVariablesNames = Object.keys(evaluatedNode.missingVariables) // Constant leaf -> search and replace the constant in all its parents. - if ( - 'valeur' in rule.rawNode || - ('formule' in rule.rawNode && missingVariablesNames.length === 0) - ) { - if ('formule' in rule.rawNode) { - ctx.parsedRules[ruleName].rawNode.valeur = formatToPulicodesValue( - nodeValue, - unit, - ) - } + if (missingVariablesNames.length === 0) { + ctx.parsedRules[ruleName].rawNode.valeur = + serializeEvaluation(evaluatedNode) searchAndReplaceConstantValueInParentRefs(ctx, ruleName, rule) if (ctx.parsedRules[ruleName] === undefined) { diff --git a/test/getRawRules.test.ts b/test/getRawRules.test.ts index 647baef..ad9811e 100644 --- a/test/getRawRules.test.ts +++ b/test/getRawRules.test.ts @@ -30,6 +30,8 @@ describe('getRawRules', () => { test3: { valeur: '10' }, }) // will be reparsed by the website client, so not a problem? }) + + // FIXME: doesn't pass with bun but passes with jest. The values seem to be equal. it('Referenced rules', () => { const rawRules = { ruleA: { diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 82abbdd..2317510 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -353,7 +353,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['omr'])).toStrictEqual({ omr: { - valeur: '0.69068', + valeur: '0.69068kgCO2e', optimized: true, }, }) From ddb7bb02b30b58ae599873af0a0b5f3c57fbc94c Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Tue, 19 Dec 2023 18:30:13 +0100 Subject: [PATCH 19/54] refactor(optim): start to implement serializeParsedRules --- source/index.ts | 1 + source/serializeParsedRules.ts | 117 +++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 source/serializeParsedRules.ts diff --git a/source/index.ts b/source/index.ts index 479c713..8d1a41d 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1 +1,2 @@ export * from './commons' +export * from './serializeParsedRules' diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts new file mode 100644 index 0000000..080f07e --- /dev/null +++ b/source/serializeParsedRules.ts @@ -0,0 +1,117 @@ +import { ASTNode, ParsedRules, reduceAST, serializeUnit } from 'publicodes' +import { RawRule, RuleName } from './commons' + +type SourceMap = { + mecanismName: string + args: Record> +} + +function serializeValue(node: ASTNode, parentSourceMap: SourceMap): any { + switch (node.nodeKind) { + case 'reference': { + return node.name + } + case 'constant': { + switch (node.type) { + case 'boolean': + return node.nodeValue ? 'oui' : 'non' + case 'string': + return `'${node.nodeValue}'` + case 'number': + return node.nodeValue + default: + // There is some 'constant' without node.type, is it a bug? + return node.nodeValue + } + } + case 'condition': { + return { + si: serializeASTNode(node.explanation.si), + alors: serializeASTNode(node.explanation.alors), + sinon: serializeASTNode(node.explanation.sinon), + } + } + case 'operation': { + if (node?.sourceMap?.mecanismName === 'est défini') { + return serializeMechanism(node) + } + return `${serializeValue(node.explanation[0], node.sourceMap)} ${ + node.operationKind + } ${serializeValue(node.explanation[1], node.sourceMap)}` + } + case 'unité': { + const unit = serializeUnit(node.unit) + const nodeValue = serializeValue(node.explanation, node.sourceMap) + + return nodeValue + (unit ? unit : '') + } + default: { + return `TODO: ${node.nodeKind}` + } + } +} + +function serializeMechanism(node: ASTNode): Record { + const sourceMap = node.sourceMap + + const rawRule = {} + for (const key in sourceMap.args) { + const value = sourceMap.args[key] + const isArray = Array.isArray(value) + + rawRule[sourceMap.mecanismName] = isArray + ? value.map((v) => serializeValue(v, sourceMap)) + : serializeValue(value, sourceMap) + } + return rawRule +} + +function serializeASTNode(node: ASTNode): RawRule { + return reduceAST( + (rawRule, node: ASTNode) => { + switch (node.nodeKind) { + default: { + if (node?.sourceMap) { + switch (node.sourceMap.mecanismName) { + case 'dans la situation': { + if (node.nodeKind === 'condition') { + return { + ...rawRule, + ...serializeASTNode(node.explanation.alors), + } + } else { + console.error( + `'dans la situation' expect be resolved to a condition got ${node.nodeKind}`, + ) + } + } + case 'une de de ces conditions': { + console.log(node.sourceMap) + } + default: { + return { ...rawRule, ...serializeMechanism(node) } + } + } + } + } + } + }, + {}, + node, + ) +} + +export function serializeParsedRules( + parsedRules: ParsedRules, +): Record { + const rawRules = {} + + for (const [rule, node] of Object.entries(parsedRules)) { + rawRules[rule] = { + ...serializeASTNode(node.explanation.valeur), + } + delete rawRules[rule].nom + } + + return rawRules +} From e0e1216dd9eabbaf47961d8a32faa8cba63cd615 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 8 Jan 2024 12:25:55 +0100 Subject: [PATCH 20/54] pkg: upgrade publicodes to rc.6 --- source/serializeParsedRules.ts | 39 ++++++++++++++++++++++++------- test/serializeParsedRules.test.ts | 20 ++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 test/serializeParsedRules.test.ts diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 080f07e..276fa14 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -6,7 +6,11 @@ type SourceMap = { args: Record> } -function serializeValue(node: ASTNode, parentSourceMap: SourceMap): any { +function serializeValue( + node: ASTNode, + parentSourceMap?: SourceMap, + needParens = false, +): any { switch (node.nodeKind) { case 'reference': { return node.name @@ -35,9 +39,13 @@ function serializeValue(node: ASTNode, parentSourceMap: SourceMap): any { if (node?.sourceMap?.mecanismName === 'est défini') { return serializeMechanism(node) } - return `${serializeValue(node.explanation[0], node.sourceMap)} ${ - node.operationKind - } ${serializeValue(node.explanation[1], node.sourceMap)}` + return ( + (needParens ? '(' : '') + + `${serializeValue(node.explanation[0], node.sourceMap, true)} ${ + node.operationKind + } ${serializeValue(node.explanation[1], node.sourceMap, true)}` + + (needParens ? ')' : '') + ) } case 'unité': { const unit = serializeUnit(node.unit) @@ -59,6 +67,7 @@ function serializeMechanism(node: ASTNode): Record { const value = sourceMap.args[key] const isArray = Array.isArray(value) + // FIXME: bug with 'une de ses conditions' with 'applicable si'. rawRule[sourceMap.mecanismName] = isArray ? value.map((v) => serializeValue(v, sourceMap)) : serializeValue(value, sourceMap) @@ -69,12 +78,26 @@ function serializeMechanism(node: ASTNode): Record { function serializeASTNode(node: ASTNode): RawRule { return reduceAST( (rawRule, node: ASTNode) => { + console.log(`\n----- serializing `, node.nodeKind) switch (node.nodeKind) { + case 'constant': { + const serializedValue = serializeValue(node) + console.log(`serializing constant`, node) + return serializedValue + } default: { if (node?.sourceMap) { switch (node.sourceMap.mecanismName) { case 'dans la situation': { if (node.nodeKind === 'condition') { + const serializedNode = serializeASTNode( + node.explanation.alors, + ) + if (typeof serializedNode !== 'object') { + return { + valeur: serializedNode, + } + } return { ...rawRule, ...serializeASTNode(node.explanation.alors), @@ -85,18 +108,16 @@ function serializeASTNode(node: ASTNode): RawRule { ) } } - case 'une de de ces conditions': { - console.log(node.sourceMap) - } default: { return { ...rawRule, ...serializeMechanism(node) } } } } + return { ...rawRule, ...serializeMechanism(node) } } } }, - {}, + {} as RawRule, node, ) } @@ -107,7 +128,9 @@ export function serializeParsedRules( const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { + console.log(`serializing rule ${node.nodeKind}`) rawRules[rule] = { + ...node.rawNode, ...serializeASTNode(node.explanation.valeur), } delete rawRules[rule].nom diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts new file mode 100644 index 0000000..5946a4c --- /dev/null +++ b/test/serializeParsedRules.test.ts @@ -0,0 +1,20 @@ +import Engine from 'publicodes' +import { serializeParsedRules } from '../source/index' + +describe('serializeParsedRules', () => { + it('should serialize empty rules', () => { + expect(serializeParsedRules({})).toStrictEqual({}) + }) + + it('should serialize simple rule with one mecanism', () => { + const rules = { + rule: { + titre: 'My rule', + valeur: 10, + }, + } + expect( + serializeParsedRules(new Engine(rules).getParsedRules()), + ).toStrictEqual(rules) + }) +}) From 78d6309b7d11cfca672360de7c4bf3aece44ce86 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 15 Jan 2024 11:15:08 +0100 Subject: [PATCH 21/54] fix: upgrade to rc.7 --- source/optims/constantFolding.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 3f1039f..ee0705e 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -4,7 +4,7 @@ import Engine, { parseExpression, serializeEvaluation, } from 'publicodes' -import type { RuleNode, ASTNode, Unit } from 'publicodes' +import type { RuleNode, ASTNode } from 'publicodes' import { getAllRefsInNode, RuleName, @@ -360,7 +360,7 @@ function updateRefCounting( if (ctx.refs.parents.get(ruleNameToUpdate)?.length === 0) { deleteRule(ctx, ruleNameToUpdate) } - }) + } } function tryToFoldRule( @@ -388,7 +388,7 @@ function tryToFoldRule( return } - const { nodeValue, missingVariables, unit } = ctx.engine.evaluateNode(rule) + const evaluatedNode = ctx.engine.evaluateNode(rule) if ('valeur' in rule.rawNode) { rule.rawNode.formule = rule.rawNode.valeur @@ -457,17 +457,14 @@ export function constantFolding( console.timeEnd('copy') console.time('initFoldingCtx') - let ctx: FoldingCtx = { - engine, - parsedRules, - toKeep, - refs: getRefs(parsedRules), - params: params?.isFoldedAttr ? params : { isFoldedAttr: 'optimized' }, - } + let ctx = initFoldingCtx(engine, parsedRules, toKeep, params) console.timeEnd('initFoldingCtx') for (const [ruleName, ruleNode] of Object.entries(ctx.parsedRules)) { - if (isFoldable(ruleNode) && !isAlreadyFolded(ctx.params, ruleNode)) { + if ( + isFoldable(ruleNode, ctx.impactedByContexteRules) && + !isAlreadyFolded(ctx.params, ruleNode) + ) { tryToFoldRule(ctx, ruleName, ruleNode) } } From 160bd314114c55232828172c0b7958582451d1a9 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 15 Jan 2024 12:48:36 +0100 Subject: [PATCH 22/54] refactor(optim): serializedParsedRules implemented and tested against the doc API examples --- source/serializeParsedRules.ts | 339 +++++++++++++++++---- test/serializeParsedRules.test.ts | 488 +++++++++++++++++++++++++++++- 2 files changed, 755 insertions(+), 72 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 276fa14..20f2bca 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -1,20 +1,29 @@ import { ASTNode, ParsedRules, reduceAST, serializeUnit } from 'publicodes' import { RawRule, RuleName } from './commons' +// +// type SourceMap = { +// mecanismName: string +// args: Record> +// } +// +type SerializedRule = RawRule | number | string -type SourceMap = { - mecanismName: string - args: Record> +function serializedRuleToRawRule(serializedRule: SerializedRule): RawRule { + if (typeof serializedRule === 'object') { + return serializedRule + } + return { + valeur: serializedRule, + } } -function serializeValue( - node: ASTNode, - parentSourceMap?: SourceMap, - needParens = false, -): any { +function serializeValue(node: ASTNode, needParens = false): SerializedRule { + // console.log('[SERIALIZE_VALUE]:', node.nodeKind, needParens) switch (node.nodeKind) { case 'reference': { return node.name } + case 'constant': { switch (node.type) { case 'boolean': @@ -22,44 +31,67 @@ function serializeValue( case 'string': return `'${node.nodeValue}'` case 'number': - return node.nodeValue - default: - // There is some 'constant' without node.type, is it a bug? - return node.nodeValue - } - } - case 'condition': { - return { - si: serializeASTNode(node.explanation.si), - alors: serializeASTNode(node.explanation.alors), - sinon: serializeASTNode(node.explanation.sinon), + return Number(node.nodeValue) + // TODO: case 'date': + default: { + return node.nodeValue?.toLocaleString('fr-FR') + } } } + case 'operation': { - if (node?.sourceMap?.mecanismName === 'est défini') { - return serializeMechanism(node) + switch (node?.sourceMap?.mecanismName) { + /* All these mecanisms are inlined with simplier ones. Therefore, + * we need to serialize the sourceMap in order to retrieve the + * original mecanism. + * + * Example: + * [une de ces conditions] is inlined with a composition of disjunctions ('ou') + * [toutes ces conditions] is inlined with a composition of conjunctions ('et') + * [somme] is inlined with a sum of values ('+') + * etc... + */ + case 'somme': + case 'moyenne': + case 'une de ces conditions': + case 'toutes ces conditions': + /* The engine parse the mecanism + * 'est défini: ' + * as + * 'est non défini: = non' + */ + case 'est défini': + case 'est applicable': { + return serializeSourceMap(node) + } + + default: { + return ( + (needParens ? '(' : '') + + `${serializeValue(node.explanation[0], true)} ${ + node.operationKind + } ${serializeValue(node.explanation[1], true)}` + + (needParens ? ')' : '') + ) + } } - return ( - (needParens ? '(' : '') + - `${serializeValue(node.explanation[0], node.sourceMap, true)} ${ - node.operationKind - } ${serializeValue(node.explanation[1], node.sourceMap, true)}` + - (needParens ? ')' : '') - ) } + case 'unité': { + console.log('[UNIT]:', node) const unit = serializeUnit(node.unit) - const nodeValue = serializeValue(node.explanation, node.sourceMap) + const nodeValue = serializeASTNode(node.explanation) - return nodeValue + (unit ? unit : '') + return nodeValue + (unit ? ' ' + unit : '') } + default: { return `TODO: ${node.nodeKind}` } } } -function serializeMechanism(node: ASTNode): Record { +function serializeSourceMap(node: ASTNode): SerializedRule { const sourceMap = node.sourceMap const rawRule = {} @@ -67,53 +99,221 @@ function serializeMechanism(node: ASTNode): Record { const value = sourceMap.args[key] const isArray = Array.isArray(value) + console.log('[SOURCE_MAP]:', key) + // FIXME: bug with 'une de ses conditions' with 'applicable si'. rawRule[sourceMap.mecanismName] = isArray - ? value.map((v) => serializeValue(v, sourceMap)) - : serializeValue(value, sourceMap) + ? value.map((v) => serializeASTNode(v)) + : serializeASTNode(value) } return rawRule } -function serializeASTNode(node: ASTNode): RawRule { - return reduceAST( +function serializeASTNode(node: ASTNode): SerializedRule { + return reduceAST( (rawRule, node: ASTNode) => { - console.log(`\n----- serializing `, node.nodeKind) - switch (node.nodeKind) { - case 'constant': { - const serializedValue = serializeValue(node) - console.log(`serializing constant`, node) - return serializedValue + // if (node?.nodeKind) { + console.log('[NODE_KIND]:', node.nodeKind) + console.log('[MECANISME_NAME]:', node?.sourceMap?.mecanismName) + // } else { + // console.log('[NODE_KIND]:', node) + // return rawRule + // } + switch (node?.nodeKind) { + case 'reference': + case 'constant': + case 'unité': + case 'operation': { + return serializeValue(node) } - default: { - if (node?.sourceMap) { - switch (node.sourceMap.mecanismName) { - case 'dans la situation': { - if (node.nodeKind === 'condition') { - const serializedNode = serializeASTNode( - node.explanation.alors, - ) - if (typeof serializedNode !== 'object') { - return { - valeur: serializedNode, - } - } - return { - ...rawRule, - ...serializeASTNode(node.explanation.alors), - } - } else { - console.error( - `'dans la situation' expect be resolved to a condition got ${node.nodeKind}`, - ) + + case 'est non défini': + case 'est non applicable': { + return { + [node.nodeKind]: serializeASTNode(node.explanation), + } + } + + // [produit] is parsed as a one big multiplication, so we need to + // gets the sourceMap to get the real mecanismName + case 'simplifier unité': { + return serializeSourceMap(node) + } + + case 'variations': { + return { + variations: node.explanation.map(({ condition, consequence }) => { + if ( + 'type' in condition && + condition.type === 'boolean' && + condition.nodeValue + ) { + return { sinon: serializeASTNode(consequence) } + } + return { + si: serializeASTNode(condition), + alors: serializeASTNode(consequence), + } + }), + } + } + + case 'arrondi': { + return { + arrondi: serializeASTNode(node.explanation.arrondi), + valeur: serializeASTNode(node.explanation.valeur), + } + } + + case 'durée': { + return { + durée: { + depuis: serializeASTNode(node.explanation.depuis), + "jusqu'à": serializeASTNode(node.explanation["jusqu'à"]), + }, + } + } + + case 'taux progressif': + case 'barème': { + const serializedNode = { + assiette: serializeASTNode(node.explanation.assiette), + tranches: node.explanation.tranches.map((tranche) => { + const res = {} + + for (const key in tranche) { + const val = tranche[key] + if (key !== 'plafond' || val.nodeValue !== Infinity) { + res[key] = serializeASTNode(tranche[key]) } } - default: { - return { ...rawRule, ...serializeMechanism(node) } + + return res + }), + } + + const serializedMultiplicateur = serializeASTNode( + node.explanation.multiplicateur, + ) + + if (serializedMultiplicateur !== 1) { + serializedNode['multiplicateur'] = serializeASTNode( + node.explanation.multiplicateur, + ) + } + + return { [node.nodeKind]: serializedNode } + } + + case 'contexte': { + const contexte = node.explanation.contexte.reduce( + (currCtx, [ref, node]) => { + currCtx[ref.name] = serializeASTNode(node) + return currCtx + }, + {}, + ) + const serializedExplanationNode = serializedRuleToRawRule( + serializeASTNode(node.explanation.node), + ) + return { + contexte, + ...serializedExplanationNode, + } + } + + case 'condition': { + const sourceMap = node?.sourceMap + const mecanismName = sourceMap?.mecanismName + switch (mecanismName) { + case 'dans la situation': { + // The engine parse all rules into a root condition: + // + // - si: + // est non défini: . $SITUATION + // - alors: + // - sinon: . $SITUATION + // + if ( + sourceMap.args['dans la situation']['title'] === '$SITUATION' + ) { + return serializeASTNode(node.explanation.alors) + } + } + + case 'applicable si': + case 'non applicable si': { + const serializedExplanationNode = serializedRuleToRawRule( + serializeASTNode(node.explanation.alors), + ) + return { + [mecanismName]: serializeASTNode( + sourceMap.args[mecanismName] as ASTNode, + ), + ...serializedExplanationNode, + } + } + + // Needs to the serialize the source map in order to retrieve the + // original mecanism. + case 'le maximum de': + case 'le minimum de': { + return serializeSourceMap(node) + } + + case 'abattement': + case 'plancher': + case 'plafond': { + const serializedExplanationNode = serializedRuleToRawRule( + serializeASTNode(node.sourceMap.args.valeur as ASTNode), + ) + + return { + [mecanismName]: serializeASTNode( + node.sourceMap.args[mecanismName] as ASTNode, + ), + ...serializedExplanationNode, } } } - return { ...rawRule, ...serializeMechanism(node) } + } + + default: { + // console.log('[DEFAULT]:', node.nodeKind) + // console.log('[KIND]:', node.nodeKind) + // console.log('[SOURCE_MAP]:', node.sourceMap) + // if (node?.sourceMap) { + // switch (node.sourceMap.mecanismName) { + // case 'dans la situation': { + // if (node.nodeKind === 'condition') { + // console.log(`\n----- serializing `, node.nodeKind) + // console.log(`----- node `, node) + // const serializedNode = serializeASTNode( + // node.explanation.alors, + // ) + // if (typeof serializedNode !== 'object') { + // return { + // valeur: serializedNode, + // } + // } + // return { + // ...rawRule, + // ...serializeASTNode(node.explanation.alors), + // } + // } else { + // console.error( + // `'dans la situation' expect be resolved to a condition got ${node.nodeKind}`, + // ) + // } + // } + // default: { + // return { ...rawRule, ...serializeMechanism(node) } + // } + // } + // } else { + // return { ...rawRule, ...serializeMechanism(node) } + // } + // return { ...rawRule, ...serializeSourceMap(node) } } } }, @@ -128,11 +328,16 @@ export function serializeParsedRules( const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { - console.log(`serializing rule ${node.nodeKind}`) + console.log(`serializing ${rule}`) + const serializedNode = serializedRuleToRawRule( + serializeASTNode(node.explanation.valeur), + ) + rawRules[rule] = { ...node.rawNode, - ...serializeASTNode(node.explanation.valeur), + ...serializedNode, } + delete rawRules[rule].nom } diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index 5946a4c..011f6fe 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -1,20 +1,498 @@ import Engine from 'publicodes' import { serializeParsedRules } from '../source/index' -describe('serializeParsedRules', () => { +describe('API > mecanisms list', () => { it('should serialize empty rules', () => { expect(serializeParsedRules({})).toStrictEqual({}) }) - it('should serialize simple rule with one mecanism', () => { + it('should serialize rule with constant [valeur]', () => { const rules = { rule: { titre: 'My rule', valeur: 10, }, } - expect( - serializeParsedRules(new Engine(rules).getParsedRules()), - ).toStrictEqual(rules) + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) }) + + it('should serialize rule with ref [valeur]', () => { + const rules = { + rule: { + titre: 'My rule', + valeur: 10, + }, + rule2: { + titre: 'Rule with a ref', + valeur: 'rule', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with ref [applicable si]', () => { + const rules = { + rule: { + 'applicable si': 'rule2', + valeur: 10, + }, + rule2: { + valeur: 20, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with condition [applicable si]', () => { + const rules = { + rule: { + 'applicable si': 'rule2 < 5', + valeur: 10, + }, + rule2: { + valeur: 20, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with condition [non applicable si]', () => { + const rules = { + rule: { + 'non applicable si': 'rule2 < 5', + valeur: 10, + }, + rule2: { + valeur: 20, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [est non défini]', () => { + const rules = { + rule: { + 'est non défini': 'rule2', + }, + rule2: { + valeur: 20, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [est défini]', () => { + const rules = { + rule: { + 'est défini': 'rule2', + }, + rule2: { + valeur: 20, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [est non applicable]', () => { + const rules = { + rule: { + 'est non applicable': 'rule2', + }, + rule2: { + valeur: 20, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [est applicable]', () => { + const rules = { + rule: { + 'est applicable': 'rule2', + }, + rule2: { + valeur: 20, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [une de ces conditions]', () => { + const rules = { + rule: { + 'une de ces conditions': ['rule2', 'rule2 < 5'], + }, + rule2: { + valeur: 20, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [toutes ces conditions]', () => { + const rules = { + rule: { + 'toutes ces conditions': ['rule2', 'rule2 < 5'], + }, + rule2: { + valeur: 20, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [produit]', () => { + const rules = { + volume: { + produit: ['2.5 m', '3 m', '4 m'], + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [produit] (without unit)', () => { + const rules = { + volume: { + produit: [2.5, 3, 4], + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [variations]', () => { + const rules = { + 'taux réduit': { + valeur: 'oui', + }, + 'taux majoré': { + valeur: 'non', + }, + 'taux allocation familiales': { + variations: [ + { + si: 'taux réduit', + alors: '3.45 %', + }, + { + si: 'taux majoré', + alors: '10 %', + }, + { sinon: '5.25 %' }, + ], + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [somme]', () => { + const rules = { + exemple: { + somme: ['15.89 €', '12 % * 14 €', '-20 €'], + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [moyenne]', () => { + const rules = { + exemple: { + moyenne: ['15.89 €', '12 % * 14 €', '-20 €'], + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [le maximum de]', () => { + const rules = { + max: { + 'le maximum de': ['15.89 €', '12 % * 14 €', '-20 €'], + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [le minimum de]', () => { + const rules = { + max: { + 'le minimum de': ['15.89 €', '12 % * 14 €', '-20 €'], + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [arrondi]', () => { + const rules = { + arrondi: { + arrondi: 'oui', + valeur: 10.5, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [arrondi] (example 2)', () => { + const rules = { + arrondi: { + arrondi: '2 décimales', + valeur: '2 / 3', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [contexte]', () => { + const rules = { + brut: { + valeur: '2000 €', + }, + cotisation: { + valeur: 'brut * 20 %', + }, + 'cotisation pour un SMIC': { + valeur: 'cotisation', + contexte: { + brut: '1500 €', + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [contexte] applied to a [somme]', () => { + const rules = { + brut: { + valeur: '2000 €', + }, + cotisation: { + valeur: 'brut * 20 %', + }, + 'cotisation pour un SMIC': { + somme: ['cotisation'], + contexte: { + brut: '1500 €', + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [barème]', () => { + const rules = { + 'revenu imposable': { + valeur: '54126 €', + }, + 'impôt sur le revenu': { + barème: { + assiette: 'revenu imposable', + tranches: [ + { taux: '0 %', plafond: '9807 €' }, + { taux: '14 %', plafond: '27086 €' }, + { taux: '30 %', plafond: '72617 €' }, + { taux: '41 %', plafond: '153783 €' }, + { taux: '45 %' }, + ], + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it("should serialize rule with [barème] and a custom 'multiplicateur'", () => { + const rules = { + 'revenu imposable': { + valeur: '54126 €', + }, + 'plafond sécurité sociale': { + valeur: '41136 €', + }, + 'impôt sur le revenu': { + barème: { + assiette: 'revenu imposable', + multiplicateur: 'plafond sécurité sociale', + tranches: [ + { taux: '0 %', plafond: '9807 €' }, + { taux: '14 %', plafond: '27086 €' }, + { taux: '30 %', plafond: '72617 €' }, + { taux: '41 %', plafond: '153783 €' }, + { taux: '45 %' }, + ], + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [taux progressif]', () => { + const rules = { + "chiffre d'affaires": { + valeur: '30000 €/an', + }, + plafond: { + valeur: '3000 €/mois', + }, + 'taux de réduction de cotisation': { + 'taux progressif': { + assiette: "chiffre d'affaires", + multiplicateur: 'plafond', + tranches: [ + { taux: '100 %', plafond: '75 %' }, + { taux: '0 %', plafond: '100 %' }, + ], + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [abattement]', () => { + const rules = { + 'revenu imposable simple': { + valeur: '30000 €', + abattement: '2000 €', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [plancher]', () => { + const rules = { + 'temperature mesurée': { + valeur: '-500 °C', + plancher: '-273.15 °C', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [plafond]', () => { + const rules = { + 'déduction fiscale': { + valeur: '1300 €/mois', + plafond: '200 €/mois', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [durée]', () => { + const rules = { + "date d'embauche": { + valeur: '14/04/2008', + }, + "ancienneté en fin d'année": { + durée: { + depuis: "date d'embauche", + "jusqu'à": '31/12/2020', + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + // TODO + // it('should serialize rule with [unité]', () => { + // const rules = { + // "date d'embauche": { + // valeur: '14/04/2008', + // }, + // "ancienneté en fin d'année": { + // durée: { + // depuis: "date d'embauche", + // "jusqu'à": '31/12/2020', + // }, + // }, + // } + // const serializedRules = serializeParsedRules( + // new Engine(rules).getParsedRules(), + // ) + // console.log(JSON.stringify(serializedRules, null, 2)) + // expect(serializedRules).toStrictEqual(rules) + // }) }) From 2dc05a836c595066853bce87e9deea8626143642 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Wed, 17 Jan 2024 11:35:40 +0100 Subject: [PATCH 23/54] =?UTF-8?q?refactor(optim):=20fix=20the=20'unit?= =?UTF-8?q?=C3=A9'=20serialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/serializeParsedRules.ts | 18 ++++++++++--- test/serializeParsedRules.test.ts | 45 ++++++++++++++++++------------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 20f2bca..f06b3c9 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -78,11 +78,21 @@ function serializeValue(node: ASTNode, needParens = false): SerializedRule { } case 'unité': { - console.log('[UNIT]:', node) - const unit = serializeUnit(node.unit) - const nodeValue = serializeASTNode(node.explanation) + const serializedUnit = serializeUnit(node.unit) + const serializedExplanation = serializeASTNode(node.explanation) + + // Inlined unit (e.g. '10 €/mois') + if (node?.explanation?.nodeKind === 'constant') { + return ( + serializedExplanation + (serializedUnit ? ' ' + serializedUnit : '') + ) + } - return nodeValue + (unit ? ' ' + unit : '') + // Explicit [unité] mecanism + return { + unité: serializedUnit, + ...serializedRuleToRawRule(serializedExplanation), + } } default: { diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index 011f6fe..6420226 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -476,23 +476,30 @@ describe('API > mecanisms list', () => { expect(serializedRules).toStrictEqual(rules) }) - // TODO - // it('should serialize rule with [unité]', () => { - // const rules = { - // "date d'embauche": { - // valeur: '14/04/2008', - // }, - // "ancienneté en fin d'année": { - // durée: { - // depuis: "date d'embauche", - // "jusqu'à": '31/12/2020', - // }, - // }, - // } - // const serializedRules = serializeParsedRules( - // new Engine(rules).getParsedRules(), - // ) - // console.log(JSON.stringify(serializedRules, null, 2)) - // expect(serializedRules).toStrictEqual(rules) - // }) + it('should serialize rule with [unité]', () => { + const rules = { + test1: { + valeur: 35, + unité: '€/mois', + }, + test2: { + valeur: '35 €/mois', + }, + test3: { + variations: [ + { si: 'test1 > 0', alors: 'test1' }, + { + sinon: { + somme: ['test1', 'test2'], + unité: '€/mois', + }, + }, + ], + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) }) From 6000bb99d4f37f3c4453fc51ee73afd1c8610de1 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Wed, 17 Jan 2024 12:41:07 +0100 Subject: [PATCH 24/54] refactor(optim): serialization implemented of all the basic API --- source/serializeParsedRules.ts | 143 +++++++++++++++++------------ test/serializeParsedRules.test.ts | 147 ++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 60 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index f06b3c9..2437ef2 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -1,11 +1,6 @@ import { ASTNode, ParsedRules, reduceAST, serializeUnit } from 'publicodes' import { RawRule, RuleName } from './commons' -// -// type SourceMap = { -// mecanismName: string -// args: Record> -// } -// + type SerializedRule = RawRule | number | string function serializedRuleToRawRule(serializedRule: SerializedRule): RawRule { @@ -18,7 +13,6 @@ function serializedRuleToRawRule(serializedRule: SerializedRule): RawRule { } function serializeValue(node: ASTNode, needParens = false): SerializedRule { - // console.log('[SERIALIZE_VALUE]:', node.nodeKind, needParens) switch (node.nodeKind) { case 'reference': { return node.name @@ -96,11 +90,12 @@ function serializeValue(node: ASTNode, needParens = false): SerializedRule { } default: { - return `TODO: ${node.nodeKind}` + throw new Error(`[SERIALIZE_VALUE]: '${node.nodeKind}' not implemented`) } } } +// TODO: this function might be refactored function serializeSourceMap(node: ASTNode): SerializedRule { const sourceMap = node.sourceMap @@ -109,9 +104,6 @@ function serializeSourceMap(node: ASTNode): SerializedRule { const value = sourceMap.args[key] const isArray = Array.isArray(value) - console.log('[SOURCE_MAP]:', key) - - // FIXME: bug with 'une de ses conditions' with 'applicable si'. rawRule[sourceMap.mecanismName] = isArray ? value.map((v) => serializeASTNode(v)) : serializeASTNode(value) @@ -121,14 +113,7 @@ function serializeSourceMap(node: ASTNode): SerializedRule { function serializeASTNode(node: ASTNode): SerializedRule { return reduceAST( - (rawRule, node: ASTNode) => { - // if (node?.nodeKind) { - console.log('[NODE_KIND]:', node.nodeKind) - console.log('[MECANISME_NAME]:', node?.sourceMap?.mecanismName) - // } else { - // console.log('[NODE_KIND]:', node) - // return rawRule - // } + (_, node: ASTNode) => { switch (node?.nodeKind) { case 'reference': case 'constant': @@ -169,9 +154,12 @@ function serializeASTNode(node: ASTNode): SerializedRule { } case 'arrondi': { + const serializedValeur = serializedRuleToRawRule( + serializeASTNode(node.explanation.valeur), + ) return { + ...serializedValeur, arrondi: serializeASTNode(node.explanation.arrondi), - valeur: serializeASTNode(node.explanation.valeur), } } @@ -227,8 +215,8 @@ function serializeASTNode(node: ASTNode): SerializedRule { serializeASTNode(node.explanation.node), ) return { - contexte, ...serializedExplanationNode, + contexte, } } @@ -257,10 +245,10 @@ function serializeASTNode(node: ASTNode): SerializedRule { serializeASTNode(node.explanation.alors), ) return { + ...serializedExplanationNode, [mecanismName]: serializeASTNode( sourceMap.args[mecanismName] as ASTNode, ), - ...serializedExplanationNode, } } @@ -273,57 +261,66 @@ function serializeASTNode(node: ASTNode): SerializedRule { case 'abattement': case 'plancher': - case 'plafond': { + case 'plafond': + case 'par défaut': { const serializedExplanationNode = serializedRuleToRawRule( serializeASTNode(node.sourceMap.args.valeur as ASTNode), ) return { + ...serializedExplanationNode, [mecanismName]: serializeASTNode( node.sourceMap.args[mecanismName] as ASTNode, ), - ...serializedExplanationNode, } } + + default: { + throw new Error( + `[SERIALIZE_AST_NODE]: mecanism '${mecanismName}' found in a '${node.nodeKind}`, + ) + } + } + } + + case 'variable manquante': { + // Simple need to unwrap the explanation node + return serializeASTNode(node.explanation) + } + + case 'texte': { + const serializedText = node.explanation.reduce( + (currText: string, node: ASTNode | string) => { + if (typeof node === 'string') { + return currText + node + } + + const serializedNode = serializeASTNode(node) + if (typeof serializedNode !== 'string') { + throw new Error(`[SERIALIZE_AST_NODE > 'texte']: all childs of 'texte' expect to be serializable as string. + Got '${serializedNode}'`) + } + return currText + '{{ ' + serializedNode + ' }}' + }, + '', + ) + return { texte: serializedText } + } + + case 'une possibilité': { + return { + 'une possibilité': { + 'choix obligatoire': node['choix obligatoire'], + possibilités: node.explanation.map(serializeASTNode), + }, } } default: { - // console.log('[DEFAULT]:', node.nodeKind) - // console.log('[KIND]:', node.nodeKind) - // console.log('[SOURCE_MAP]:', node.sourceMap) - // if (node?.sourceMap) { - // switch (node.sourceMap.mecanismName) { - // case 'dans la situation': { - // if (node.nodeKind === 'condition') { - // console.log(`\n----- serializing `, node.nodeKind) - // console.log(`----- node `, node) - // const serializedNode = serializeASTNode( - // node.explanation.alors, - // ) - // if (typeof serializedNode !== 'object') { - // return { - // valeur: serializedNode, - // } - // } - // return { - // ...rawRule, - // ...serializeASTNode(node.explanation.alors), - // } - // } else { - // console.error( - // `'dans la situation' expect be resolved to a condition got ${node.nodeKind}`, - // ) - // } - // } - // default: { - // return { ...rawRule, ...serializeMechanism(node) } - // } - // } - // } else { - // return { ...rawRule, ...serializeMechanism(node) } - // } - // return { ...rawRule, ...serializeSourceMap(node) } + throw new Error( + `[SERIALIZE_AST_NODE]: '${node.nodeKind}' not implemented. + Node:\n${JSON.stringify(node, null, 2)}`, + ) } } }, @@ -335,14 +332,40 @@ function serializeASTNode(node: ASTNode): SerializedRule { export function serializeParsedRules( parsedRules: ParsedRules, ): Record { + /** + * This mecanisms are syntaxique sugars that are inlined with simplier ones. + * Consequently, we need to remove them from the rawNode in order to avoid + * duplicate mecanisms. + * + * + * NOTE: for now, the [avec] mecanism is unfolded as full rules. Therefore, + * we need to remove the [avec] mecanism from the rawNode in order to + * avoid duplicate rule definitions. + * + * TODO: a way to keep the [avec] mecanism in the rawNode could be investigated but + * for now it's not a priority. + */ + const syntaxiqueSugars = ['avec', 'formule'] const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { - console.log(`serializing ${rule}`) + if (Object.keys(node.rawNode).length === 0) { + console.log(`[SERIALIZE_PARSED_RULES]: empty rule '${rule}' found`) + // Empty rule should be null not {} + rawRules[rule] = null + continue + } + const serializedNode = serializedRuleToRawRule( serializeASTNode(node.explanation.valeur), ) + syntaxiqueSugars.forEach((attr) => { + if (attr in node.rawNode) { + delete node.rawNode[attr] + } + }) + rawRules[rule] = { ...node.rawNode, ...serializedNode, diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index 6420226..c5c57e4 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -502,4 +502,151 @@ describe('API > mecanisms list', () => { ) expect(serializedRules).toStrictEqual(rules) }) + + it('should serialize rule with [par défaut]', () => { + const rules = { + 'prix HT': { + valeur: '50 €', + }, + 'prix TTC': { + assiette: 'prix HT * (100 % + TVA)', + }, + TVA: { + valeur: '50 %', + 'par défaut': '20 %', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [avec]', () => { + const rules = { + 'prix final': { + valeur: 'prix de base * (100% - réduction)', + avec: { + 'prix de base': '157 €', + réduction: '20 %', + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual({ + 'prix final': { + valeur: 'prix de base * (100 % - réduction)', + }, + 'prix final . prix de base': { + valeur: '157 €', + }, + 'prix final . réduction': { + valeur: '20 %', + }, + }) + }) + + it('should serialize rule with [texte]', () => { + const rules = { + 'aide vélo': { + texte: `La région subventionne l’achat d’un vélo à hauteur de + {{ prise en charge }} et jusqu’à un plafond de {{ 500 € }}. + Les éventuelles aides locales déjà perçues sont déduites de + ce montant. + + Par exemple, pour un vélo de {{ exemple }}, la région {{ 'Nouvelle Aquitaine' }} + vous versera {{ exemple * prise en charge }}`, + }, + 'aide vélo . prise en charge': { + valeur: '50 %', + }, + 'aide vélo . exemple': { + valeur: '250 €', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with multiple [mécanismes chaînés]', () => { + const rules = { + 'nombre de repas': { + valeur: '12 repas', + }, + 'remboursement repas': { + valeur: 'nombre de repas * 4.21 €/repas', + plafond: '500 €', + abattement: '25 €', + arrondi: 'oui', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with multiple [une possibilité]', () => { + const rules = { + 'chauffage collectif': { + avec: { + collectif: null, + individuel: null, + }, + question: 'Votre chauffage est-il collectif ou individuel ?', + 'par défaut': "'collectif'", + formule: { + 'une possibilité': { + 'choix obligatoire': 'oui', + possibilités: ['collectif', 'individuel'], + }, + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual({ + 'chauffage collectif': { + question: 'Votre chauffage est-il collectif ou individuel ?', + 'par défaut': "'collectif'", + 'une possibilité': { + 'choix obligatoire': 'oui', + possibilités: ['collectif', 'individuel'], + }, + }, + 'chauffage collectif . collectif': null, + 'chauffage collectif . individuel': null, + }) + }) + + // TODO + // it('should serialize rule with [private rule]', () => { + // const rules = { + // 'aide vélo': { + // texte: `La région subventionne l’achat d’un vélo à hauteur de + // {{ prise en charge }} et jusqu’à un plafond de {{ 500 € }}. + // Les éventuelles aides locales déjà perçues sont déduites de + // ce montant. + // + // Par exemple, pour un vélo de {{ exemple }}, la région {{ 'Nouvelle Aquitaine' }} + // vous versera {{ exemple * prise en charge }}`, + // }, + // 'aide vélo . prise en charge': { + // valeur: '50 %', + // }, + // 'aide vélo . exemple': { + // valeur: '250 €', + // }, + // } + // const serializedRules = serializeParsedRules( + // new Engine(rules).getParsedRules(), + // ) + // console.log(JSON.stringify(serializedRules, null, 2)) + // expect(serializedRules).toStrictEqual(rules) + // }) }) From ca5454be3df7d044327608fc55b9d24b06229739 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Wed, 17 Jan 2024 12:46:50 +0100 Subject: [PATCH 25/54] refactor(optim): add the missing 'grille' mecanism for serialization --- source/serializeParsedRules.ts | 6 ++-- test/serializeParsedRules.test.ts | 53 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 2437ef2..348b55f 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -172,8 +172,9 @@ function serializeASTNode(node: ASTNode): SerializedRule { } } - case 'taux progressif': - case 'barème': { + case 'barème': + case 'grille': + case 'taux progressif': { const serializedNode = { assiette: serializeASTNode(node.explanation.assiette), tranches: node.explanation.tranches.map((tranche) => { @@ -350,7 +351,6 @@ export function serializeParsedRules( for (const [rule, node] of Object.entries(parsedRules)) { if (Object.keys(node.rawNode).length === 0) { - console.log(`[SERIALIZE_PARSED_RULES]: empty rule '${rule}' found`) // Empty rule should be null not {} rawRules[rule] = null continue diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index c5c57e4..127bbbf 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -36,6 +36,30 @@ describe('API > mecanisms list', () => { expect(serializedRules).toStrictEqual(rules) }) + it('should serialize rule with constant [formule]', () => { + const rules = { + rule: { + titre: 'My rule', + formule: '10 * rule2', + }, + rule2: { + valeur: 2, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual({ + rule: { + titre: 'My rule', + valeur: '10 * rule2', + }, + rule2: { + valeur: 2, + }, + }) + }) + it('should serialize rule with ref [applicable si]', () => { const rules = { rule: { @@ -366,6 +390,35 @@ describe('API > mecanisms list', () => { expect(serializedRules).toStrictEqual(rules) }) + it('should serialize rule with [grille]', () => { + const rules = { + 'SMIC horaire': { + valeur: '10 €/heures', + }, + 'revenu cotisé': { + valeur: '1900 €/an', + }, + 'trimestres validés': { + unité: 'trimestres validés/an', + grille: { + assiette: 'revenu cotisé', + multiplicateur: 'SMIC horaire', + tranches: [ + { montant: 0, plafond: '150 heures/an' }, + { montant: 1, plafond: '300 heures/an' }, + { montant: 2, plafond: '450 heures/an' }, + { montant: 3, plafond: '600 heures/an' }, + { montant: 4 }, + ], + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + it("should serialize rule with [barème] and a custom 'multiplicateur'", () => { const rules = { 'revenu imposable': { From dc027ff4ca95c2dfa0fe4739626cca473ebf1dcd Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Wed, 17 Jan 2024 13:16:06 +0100 Subject: [PATCH 26/54] fix(optim): 'nom' doesn't exist in 'rawNode' anymore --- source/serializeParsedRules.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 348b55f..8e88dc4 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -370,8 +370,6 @@ export function serializeParsedRules( ...node.rawNode, ...serializedNode, } - - delete rawRules[rule].nom } return rawRules From 08d018ec5990e4bd6e84a6f71d27ddebbb339948 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Wed, 17 Jan 2024 13:17:17 +0100 Subject: [PATCH 27/54] nitpick(opti): unify comment format --- source/serializeParsedRules.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 8e88dc4..041a768 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -35,7 +35,8 @@ function serializeValue(node: ASTNode, needParens = false): SerializedRule { case 'operation': { switch (node?.sourceMap?.mecanismName) { - /* All these mecanisms are inlined with simplier ones. Therefore, + /* + * All these mecanisms are inlined with simplier ones. Therefore, * we need to serialize the sourceMap in order to retrieve the * original mecanism. * @@ -49,7 +50,8 @@ function serializeValue(node: ASTNode, needParens = false): SerializedRule { case 'moyenne': case 'une de ces conditions': case 'toutes ces conditions': - /* The engine parse the mecanism + /* + * The engine parse the mecanism * 'est défini: ' * as * 'est non défini: = non' @@ -226,13 +228,14 @@ function serializeASTNode(node: ASTNode): SerializedRule { const mecanismName = sourceMap?.mecanismName switch (mecanismName) { case 'dans la situation': { - // The engine parse all rules into a root condition: - // - // - si: - // est non défini: . $SITUATION - // - alors: - // - sinon: . $SITUATION - // + /* + * The engine parse all rules into a root condition: + * + * - si: + * est non défini: . $SITUATION + * - alors: + * - sinon: . $SITUATION + */ if ( sourceMap.args['dans la situation']['title'] === '$SITUATION' ) { From e1248c369feb2687f9901b822a7f728b4f9c7e7d Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 18 Jan 2024 13:41:49 +0100 Subject: [PATCH 28/54] refactor(test/optim): set allowOrphanRules to true for the engine --- test/optims/constantFolding.test.ts | 39 +++++++++++++++++------------ test/utils.test.ts | 5 +++- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 2317510..2fd3187 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -27,7 +27,6 @@ describe('Constant folding [meta]', () => { titre: 'Rule A', formule: 'B . C * D', }, - 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -35,7 +34,10 @@ describe('Constant folding [meta]', () => { valeur: '3', }, } - const engine = new Engine(rawRules, { logger: disabledLogger }) + const engine = new Engine(rawRules, { + logger: disabledLogger, + allowOrphanRules: true, + }) const untouchedParsedRules = getRawNodes(engine.getParsedRules()) constantFolding(engine, ([ruleName, _]) => ruleName === 'ruleA') @@ -50,10 +52,10 @@ describe('Constant folding [base]', () => { it('∅ -> ∅', () => { expect(constantFoldingWith({})).toStrictEqual({}) }) + it('should remove empty nodes', () => { expect( constantFoldingWith({ - ruleA: null, ruleB: { formule: '10 * 10', }, @@ -65,13 +67,13 @@ describe('Constant folding [base]', () => { }, }) }) + it('should replace a [formule] with 1 dependency with the corresponding constant value', () => { const rawRules = { ruleA: { titre: 'Rule A', formule: 'B . C * 3', }, - 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -84,13 +86,13 @@ describe('Constant folding [base]', () => { }, }) }) + it('should replace a [formule] with 2 dependencies with the corresponding constant value', () => { const rawRules = { ruleA: { titre: 'Rule A', formule: 'B . C * D', }, - 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -106,6 +108,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should replace the constant reference without being able to fold entirely the rule', () => { const rawRules = { ruleA: { @@ -115,7 +118,6 @@ describe('Constant folding [base]', () => { 'ruleA . D': { question: "What's the value of D", }, - 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -131,6 +133,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should partially fold rule with constant with multiple parents dependencies', () => { const rawRules = { ruleA: { @@ -143,7 +146,6 @@ describe('Constant folding [base]', () => { 'ruleA . D': { question: "What's the value of D?", }, - 'ruleA . B': null, 'ruleA . B . C': { valeur: '10', }, @@ -159,13 +161,13 @@ describe('Constant folding [base]', () => { }, }) }) + it('should partially fold rule with constant with multiple parents dependencies add keep the only targeted rule: [ruleA]', () => { const rawRules = { ruleA: { titre: 'Rule A', formule: 'B . C * D', }, - 'ruleA . B': null, ruleB: { formule: 'ruleA . B . C * 3', }, @@ -187,6 +189,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should fold a constant within _two degrees_', () => { const rawRules = { A: { @@ -206,6 +209,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should fold constant within two degrees with B, a partially foldable rule', () => { const rawRules = { A: { @@ -234,6 +238,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should completely fold a [somme] mechanism', () => { const rawRules = { ruleA: { @@ -242,7 +247,6 @@ describe('Constant folding [base]', () => { ruleB: { somme: ['A . B * 2', 10, 12 * 2], }, - A: null, 'A . B': { formule: 'C * 10', }, @@ -257,6 +261,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should partially fold [formule > somme] mechanism', () => { const rawRules = { ruleA: { @@ -270,7 +275,6 @@ describe('Constant folding [base]', () => { 'ruleB . D': { question: "What's the value of ruleB . D?", }, - A: null, 'A . B': { formule: 'C * 10', }, @@ -293,6 +297,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should fold a mutiple [somme] deep dependencies', () => { const rawRules = { omr: { @@ -358,6 +363,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should replace properly child rule references when one is a substring of the other: (Ambiguity with rule name)', () => { const rawRules = { biogaz: { @@ -367,7 +373,6 @@ describe('Constant folding [base]', () => { "biogaz . facteur d'émission": { valeur: 20, }, - gaz: null, "gaz . facteur d'émission": { valeur: 10, }, @@ -385,6 +390,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('replaceAllRefs bug #1', () => { const rawRules = { biogaz: { @@ -394,7 +400,6 @@ describe('Constant folding [base]', () => { "biogaz . facteur d'émission": { valeur: 20, }, - gaz: null, "gaz . facteur d'émission": { valeur: 10, }, @@ -412,6 +417,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('replaceAllRefs bug #2', () => { const rawRules = { boisson: { @@ -434,6 +440,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should fold standalone [formule] rule', () => { const rawRules = { boisson: 'tasse de café * nombre', @@ -454,6 +461,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should keeps % when folding', () => { const rawRules = { boisson: 'pct * nombre', @@ -474,6 +482,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('par défaut = 0', () => { const rawRules = { 'chocolat chaud': { @@ -498,6 +507,7 @@ describe('Constant folding [base]', () => { }, }) }) + it('should replace constant ref, even if it starts with diacritic', () => { const rawRules = { piscine: { @@ -519,12 +529,9 @@ describe('Constant folding [base]', () => { 'piscine . nombre': { question: 'Combien ?', 'par défaut': 2 }, }) }) + it('should work with parentheses inside [formule]', () => { const rawRules = { - divers: null, - 'divers . ameublement': null, - 'divers . ameublement . meubles': null, - 'divers . ameublement . meubles . armoire': null, 'divers . ameublement . meubles . armoire . empreinte amortie': { titre: 'Empreinte armoire amortie', formule: 'armoire . empreinte / (durée * coefficient préservation)', diff --git a/test/utils.test.ts b/test/utils.test.ts index c1d104a..58fa921 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -3,7 +3,10 @@ import Engine from 'publicodes' import type { ParsedRules } from 'publicodes' export function callWithEngine(fn: (engine: Engine) => R, rawRules: any): R { - const engine = new Engine(rawRules, { logger: disabledLogger }) + const engine = new Engine(rawRules, { + logger: disabledLogger, + allowOrphanRules: true, + }) return fn(engine) } From 0993335bfb03bcbf1a028d671780ad72461ba823 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 18 Jan 2024 16:15:08 +0100 Subject: [PATCH 29/54] feat(optim): regression tests pass for the new optim --- source/optims/constantFolding.ts | 204 +++++++++++++----------- source/serializeParsedRules.ts | 13 +- test/optims/constantFolding.test.ts | 233 ++++++++++++++-------------- test/serializeParsedRules.test.ts | 47 ++++++ 4 files changed, 281 insertions(+), 216 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index ee0705e..18dc4b6 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -2,7 +2,9 @@ import Engine, { reduceAST, ParsedRules, parseExpression, - serializeEvaluation, + transformAST, + traverseASTNode, + Unit, } from 'publicodes' import type { RuleNode, ASTNode } from 'publicodes' import { @@ -208,67 +210,67 @@ function lexicalSubstitutionOfRefValue( parent: RuleNode, constant: RuleNode, ): RuleNode | undefined { - // Retrieves the name form used in the rule. For exemple, the rule 'root . a - // . b' could have the name 'b', 'a . b' or 'root . a . b'. - // const substituteAST = transformAST((node, transform) => { - // // if - // }) - - const refName = reduceAST( - (_, node: ASTNode) => { + const newNode = traverseASTNode( + transformAST((node, _) => { if ( node.nodeKind === 'reference' && node.dottedName === constant.dottedName ) { - return node.name + // TODO: needs to be extracted in a function checking if a node is a + // constant node. + // @ts-ignore + return constant.explanation.valeur.explanation.alors } - }, - '', + }), parent, ) - const constValue = constant.rawNode.valeur - - // NOTE: here we directly replace the [rawNode] as it's what we get back with [getRawNodes] - // at the end. - // Instead, we could transform the complete parsed rule and serialize it at the end. + return newNode as RuleNode // - // If I continue to transform directly the [rawNode] then I can use directly the - // rules given to the engine and no need to make a deep copy therefore. We simply - // need to add the dottedName info in the rawRule. - if ('formule' in parent.rawNode) { - if (typeof parent.rawNode.formule === 'string') { - const newFormule = replaceAllRefs( - parent.rawNode.formule, - refName, - constValue, - constant.dottedName, - ) - parent.rawNode.formule = newFormule - return parent - } else if ('somme' in parent.rawNode.formule) { - // TODO: needs to be abstracted - parent.rawNode.formule.somme = ( - parent.rawNode.formule.somme as (string | number)[] - ).map((expr: string | number) => { - return typeof expr === 'string' - ? replaceAllRefs(expr, refName, constValue, constant.dottedName) - : expr - }) - return parent - } - } - // When a rule defined as an unique string: 'var * var2', it's parsed as a [valeur] attribute not a [formule]. - if (typeof parent.rawNode.valeur === 'string') { - parent.rawNode.formule = replaceAllRefs( - parent.rawNode.valeur, - refName, - constValue, - constant.dottedName, - ) - delete parent.rawNode.valeur - return parent - } + // const refName = reduceAST((_, node: ASTNode) => {}, '', parent) + // + // const constValue = constant.rawNode.valeur + // + // // NOTE: here we directly replace the [rawNode] as it's what we get back with [getRawNodes] + // // at the end. + // // Instead, we could transform the complete parsed rule and serialize it at the end. + // // + // // If I continue to transform directly the [rawNode] then I can use directly the + // // rules given to the engine and no need to make a deep copy therefore. We simply + // // need to add the dottedName info in the rawRule. + // if ('formule' in parent.rawNode) { + // if (typeof parent.rawNode.formule === 'string') { + // const newFormule = replaceAllRefs( + // parent.rawNode.formule, + // refName, + // constValue, + // constant.dottedName, + // ) + // parent.rawNode.formule = newFormule + // return parent + // } else if ('somme' in parent.rawNode.formule) { + // // TODO: needs to be abstracted + // parent.rawNode.formule.somme = ( + // parent.rawNode.formule.somme as (string | number)[] + // ).map((expr: string | number) => { + // return typeof expr === 'string' + // ? replaceAllRefs(expr, refName, constValue, constant.dottedName) + // : expr + // }) + // return parent + // } + // } + // // When a rule defined as an unique string: 'var * var2', it's parsed as a [valeur] attribute not a [formule]. + // if (typeof parent.rawNode.valeur === 'string') { + // parent.rawNode.formule = replaceAllRefs( + // parent.rawNode.valeur, + // refName, + // constValue, + // constant.dottedName, + // ) + // delete parent.rawNode.valeur + // return parent + // } } /** Replaces all references in parent refs of [ruleName] by its [rule.valeur] */ @@ -281,13 +283,13 @@ function searchAndReplaceConstantValueInParentRefs( if (refs) { for (const parentName of refs) { - let parentRule = ctx.parsedRules[parentName] + const parentRule = ctx.parsedRules[parentName] if (isFoldable(parentRule, ctx.impactedByContexteRules)) { const newRule = lexicalSubstitutionOfRefValue(parentRule, rule) if (newRule !== undefined) { - parentRule = newRule - parentRule.rawNode[ctx.params.isFoldedAttr] = true + ctx.parsedRules[parentName] = newRule + ctx.parsedRules[parentName].rawNode[ctx.params.isFoldedAttr] = true removeInMap(ctx.refs.parents, ruleName, parentName) } } @@ -314,7 +316,7 @@ function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: RuleName[]) { isFoldable(childNode, ctx.impactedByContexteRules) && !isAlreadyFolded(ctx.params, childNode) ) { - tryToFoldRule(ctx, childName, childNode) + fold(ctx, childName, childNode) } } } @@ -363,11 +365,43 @@ function updateRefCounting( } } -function tryToFoldRule( - ctx: FoldingCtx, - ruleName: RuleName, - rule: RuleNode, -): void { +function replaceRuleWithEvaluatedNodeValue( + rule: ASTNode, + nodeValue: number | boolean | string | Record, + unit: Unit | undefined, +) { + const constantNode = { + nodeValue, + type: typeof nodeValue, + nodeKind: 'constant', + missingVariables: {}, + rawNode: { + valeur: nodeValue, + }, + fullPrecision: true, + isNullable: false, + isActive: true, + } + // + // The engine parse all rules into a root condition: + // + // - si: + // est non défini: . $SITUATION + // - alors: + // - sinon: . $SITUATION + // + // @ts-ignore + rule.explanation.valeur.explanation.alors = + unit !== undefined + ? { + nodeKind: 'unité', + unit, + explanation: constantNode, + } + : constantNode +} + +function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { if ( rule !== undefined && (!isFoldable(rule, ctx.impactedByContexteRules) || @@ -388,50 +422,37 @@ function tryToFoldRule( return } - const evaluatedNode = ctx.engine.evaluateNode(rule) - - if ('valeur' in rule.rawNode) { - rule.rawNode.formule = rule.rawNode.valeur - delete rule.rawNode.valeur - } + const { missingVariables, nodeValue, unit } = ctx.engine.evaluateNode(rule) - const missingVariablesNames = Object.keys(evaluatedNode.missingVariables) + const missingVariablesNames = Object.keys(missingVariables) // Constant leaf -> search and replace the constant in all its parents. if (missingVariablesNames.length === 0) { - ctx.parsedRules[ruleName].rawNode.valeur = - serializeEvaluation(evaluatedNode) + replaceRuleWithEvaluatedNodeValue(rule, nodeValue, unit) searchAndReplaceConstantValueInParentRefs(ctx, ruleName, rule) if (ctx.parsedRules[ruleName] === undefined) { return } - if ('formule' in rule.rawNode) { - // The rule do not depends on any other rule anymore, so we need to remove - // it from the [refs]. - const childs = ctx.refs.childs.get(ruleName) ?? [] + const childs = ctx.refs.childs.get(ruleName) ?? [] - updateRefCounting(ctx, ruleName, childs) - delete ctx.parsedRules[ruleName].rawNode.formule - } + updateRefCounting(ctx, ruleName, childs) + delete ctx.parsedRules[ruleName].rawNode.formule if (ctx.refs.parents.get(ruleName)?.length === 0) { - // NOTE(@EmileRolley): temporary work around until all mechanisms are supported. - // Indeed, when replacing a leaf ref by its value in all its parents, - // it should always be removed. deleteRule(ctx, ruleName) } else { ctx.parsedRules[ruleName].rawNode[ctx.params.isFoldedAttr] = true } return - } else if ('formule' in rule.rawNode) { - // Try to replace internal refs if possible. - const childs = ctx.refs.childs.get(ruleName) - if (childs?.length > 0) { - replaceAllPossibleChildRefs(ctx, childs) - } + } else { + // // Try to replace internal refs if possible. + // const childs = ctx.refs.childs.get(ruleName) + // if (childs?.length > 0) { + // replaceAllPossibleChildRefs(ctx, childs) + // } } } @@ -450,22 +471,19 @@ export function constantFolding( toKeep?: PredicateOnRule, params?: FoldingParams, ): ParsedRules { - console.time('copy') const parsedRules: ParsedRules = // PERF: could it be avoided? JSON.parse(JSON.stringify(engine.getParsedRules())) - console.timeEnd('copy') - console.time('initFoldingCtx') let ctx = initFoldingCtx(engine, parsedRules, toKeep, params) - console.timeEnd('initFoldingCtx') - for (const [ruleName, ruleNode] of Object.entries(ctx.parsedRules)) { + for (const ruleName in ctx.parsedRules) { + const ruleNode = ctx.parsedRules[ruleName] if ( isFoldable(ruleNode, ctx.impactedByContexteRules) && !isAlreadyFolded(ctx.params, ruleNode) ) { - tryToFoldRule(ctx, ruleName, ruleNode) + fold(ctx, ruleName, ruleNode) } } diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 041a768..f22980d 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -337,7 +337,7 @@ export function serializeParsedRules( parsedRules: ParsedRules, ): Record { /** - * This mecanisms are syntaxique sugars that are inlined with simplier ones. + * This mecanisms are syntaxic sugars that are inlined with simplier ones. * Consequently, we need to remove them from the rawNode in order to avoid * duplicate mecanisms. * @@ -349,7 +349,7 @@ export function serializeParsedRules( * TODO: a way to keep the [avec] mecanism in the rawNode could be investigated but * for now it's not a priority. */ - const syntaxiqueSugars = ['avec', 'formule'] + const syntaxicSugars = ['avec', 'formule', 'valeur'] const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { @@ -363,14 +363,15 @@ export function serializeParsedRules( serializeASTNode(node.explanation.valeur), ) - syntaxiqueSugars.forEach((attr) => { - if (attr in node.rawNode) { - delete node.rawNode[attr] + rawRules[rule] = { ...node.rawNode } + syntaxicSugars.forEach((attr) => { + if (attr in rawRules[rule]) { + delete rawRules[rule][attr] } }) rawRules[rule] = { - ...node.rawNode, + ...rawRules[rule], ...serializedNode, } } diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 2fd3187..bde4e9d 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -1,10 +1,6 @@ import Engine from 'publicodes' -import { - getRawNodes, - RuleName, - RawRules, - disabledLogger, -} from '../../source/commons' +import { serializeParsedRules } from '../../source' +import { RuleName, RawRules, disabledLogger } from '../../source/commons' import { constantFolding } from '../../source/optims/' import { callWithEngine } from '../utils.test' @@ -17,7 +13,7 @@ function constantFoldingWith(rawRules: any, targets?: RuleName[]): RawRules { ), rawRules, ) - return getRawNodes(res) + return serializeParsedRules(res) } describe('Constant folding [meta]', () => { @@ -25,7 +21,7 @@ describe('Constant folding [meta]', () => { const rawRules = { ruleA: { titre: 'Rule A', - formule: 'B . C * D', + valeur: 'B . C * D', }, 'ruleA . B . C': { valeur: '10', @@ -38,12 +34,19 @@ describe('Constant folding [meta]', () => { logger: disabledLogger, allowOrphanRules: true, }) - const untouchedParsedRules = getRawNodes(engine.getParsedRules()) + const baseParsedRules = engine.getParsedRules() + const serializedBaseParsedRules = serializeParsedRules(baseParsedRules) constantFolding(engine, ([ruleName, _]) => ruleName === 'ruleA') - expect(getRawNodes(engine.getParsedRules())).toStrictEqual( - untouchedParsedRules, + const shouldNotBeModifiedRules = engine.getParsedRules() + const serializedShouldNotBeModifiedRules = serializeParsedRules( + shouldNotBeModifiedRules, + ) + + expect(baseParsedRules).toStrictEqual(shouldNotBeModifiedRules) + expect(serializedBaseParsedRules).toStrictEqual( + serializedShouldNotBeModifiedRules, ) }) }) @@ -57,22 +60,22 @@ describe('Constant folding [base]', () => { expect( constantFoldingWith({ ruleB: { - formule: '10 * 10', + valeur: '10 * 10', }, }), ).toStrictEqual({ ruleB: { - valeur: '100', + valeur: 100, optimized: true, }, }) }) - it('should replace a [formule] with 1 dependency with the corresponding constant value', () => { + it('one deps', () => { const rawRules = { ruleA: { titre: 'Rule A', - formule: 'B . C * 3', + valeur: 'B . C * 3', }, 'ruleA . B . C': { valeur: '10', @@ -81,17 +84,17 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ ruleA: { titre: 'Rule A', - valeur: '30', + valeur: 30, optimized: true, }, }) }) - it('should replace a [formule] with 2 dependencies with the corresponding constant value', () => { + it('should replace a [valeur] with 2 dependencies with the corresponding constant value', () => { const rawRules = { ruleA: { titre: 'Rule A', - formule: 'B . C * D', + valeur: 'B . C * D', }, 'ruleA . B . C': { valeur: '10', @@ -103,7 +106,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ ruleA: { titre: 'Rule A', - valeur: '30', + valeur: 30, optimized: true, }, }) @@ -113,7 +116,7 @@ describe('Constant folding [base]', () => { const rawRules = { ruleA: { titre: 'Rule A', - formule: 'B . C * D', + valeur: 'B . C * D', }, 'ruleA . D': { question: "What's the value of D", @@ -125,7 +128,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ ruleA: { titre: 'Rule A', - formule: '10 * D', + valeur: '10 * D', optimized: true, }, 'ruleA . D': { @@ -138,10 +141,10 @@ describe('Constant folding [base]', () => { const rawRules = { ruleA: { titre: 'Rule A', - formule: 'B . C * D', + valeur: 'B . C * D', }, ruleB: { - formule: 'ruleA . B . C * 3', + valeur: 'ruleA . B . C * 3', }, 'ruleA . D': { question: "What's the value of D?", @@ -153,7 +156,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ ruleA: { titre: 'Rule A', - formule: '10 * D', + valeur: '10 * D', optimized: true, }, 'ruleA . D': { @@ -166,10 +169,10 @@ describe('Constant folding [base]', () => { const rawRules = { ruleA: { titre: 'Rule A', - formule: 'B . C * D', + valeur: 'B . C * D', }, ruleB: { - formule: 'ruleA . B . C * 3', + valeur: 'ruleA . B . C * 3', }, 'ruleA . D': { question: "What's the value of D?", @@ -181,7 +184,7 @@ describe('Constant folding [base]', () => { expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ ruleA: { titre: 'Rule A', - formule: '10 * D', + valeur: '10 * D', optimized: true, }, 'ruleA . D': { @@ -193,10 +196,10 @@ describe('Constant folding [base]', () => { it('should fold a constant within _two degrees_', () => { const rawRules = { A: { - formule: 'B', + valeur: 'B', }, 'A . B': { - formule: 'C * 10', + valeur: 'C * 10', }, 'A . B . C': { valeur: 7, @@ -204,7 +207,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['A'])).toStrictEqual({ A: { - valeur: '70', + valeur: 70, optimized: true, }, }) @@ -213,16 +216,16 @@ describe('Constant folding [base]', () => { it('should fold constant within two degrees with B, a partially foldable rule', () => { const rawRules = { A: { - formule: 'B', + valeur: 'B', }, B: { - formule: 'A . B * D', + valeur: 'A . B * D', }, 'B . D': { question: "What's the value of B . D?", }, 'A . B': { - formule: 'C * 10', + valeur: 'C * 10', }, 'A . B . C': { valeur: 7, @@ -230,7 +233,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['B'])).toStrictEqual({ B: { - formule: '70 * D', + valeur: '70 * D', optimized: true, }, 'B . D': { @@ -242,13 +245,13 @@ describe('Constant folding [base]', () => { it('should completely fold a [somme] mechanism', () => { const rawRules = { ruleA: { - formule: 'ruleB', + valeur: 'ruleB', }, ruleB: { somme: ['A . B * 2', 10, 12 * 2], }, 'A . B': { - formule: 'C * 10', + valeur: 'C * 10', }, 'A . B . C': { valeur: 7, @@ -256,19 +259,19 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ ruleA: { - valeur: '174', + valeur: 174, optimized: true, }, }) }) - it('should partially fold [formule > somme] mechanism', () => { + it('should partially fold [valeur > somme] mechanism', () => { const rawRules = { ruleA: { - formule: 'ruleB', + valeur: 'ruleB', }, ruleB: { - formule: { + valeur: { somme: ['A . B * D', 10, 12 * 2], }, }, @@ -276,7 +279,7 @@ describe('Constant folding [base]', () => { question: "What's the value of ruleB . D?", }, 'A . B': { - formule: 'C * 10', + valeur: 'C * 10', }, 'A . B . C': { valeur: 7, @@ -284,12 +287,10 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ ruleA: { - formule: 'ruleB', + valeur: 'ruleB', }, ruleB: { - formule: { - somme: ['70 * D', 10, 24], - }, + somme: ['70 * D', 10, 24], optimized: true, }, 'ruleB . D': { @@ -301,64 +302,64 @@ describe('Constant folding [base]', () => { it('should fold a mutiple [somme] deep dependencies', () => { const rawRules = { omr: { - formule: { + valeur: { somme: ['omr . putrescibles', 'omr . papier carton'], }, }, 'omr . putrescibles': { - formule: { + valeur: { somme: ['stockage', 'incinération'], }, }, 'omr . putrescibles . stockage': { - formule: 'stockage . pourcentage * stockage . impact', + valeur: 'stockage . pourcentage * stockage . impact', unité: 'kgCO2e', }, 'omr . putrescibles . stockage . pourcentage': { - formule: '24%', + valeur: '24%', }, 'omr . putrescibles . stockage . impact': { - formule: 0.692, + valeur: 0.692, unité: 'kgCO2e/kg', }, 'omr . putrescibles . incinération': { - formule: 'incinération . pourcentage * incinération . impact', + valeur: 'incinération . pourcentage * incinération . impact', unité: 'kgCO2e', }, 'omr . putrescibles . incinération . pourcentage': { - formule: '68%', + valeur: '68%', }, 'omr . putrescibles . incinération . impact': { - formule: 0.045, + valeur: 0.045, unité: 'kgCO2e/kg', }, 'omr . papier carton': { - formule: { + valeur: { somme: ['stockage', 'incinération'], }, }, 'omr . papier carton . stockage': { - formule: 'stockage . pourcentage * stockage . impact', + valeur: 'stockage . pourcentage * stockage . impact', }, 'omr . papier carton . stockage . pourcentage': { - formule: '26%', + valeur: '26%', }, 'omr . papier carton . stockage . impact': { - formule: 0.95, + valeur: 0.95, }, 'omr . papier carton . incinération': { - formule: 'incinération . pourcentage * incinération . impact', + valeur: 'incinération . pourcentage * incinération . impact', }, 'omr . papier carton . incinération . pourcentage': { - formule: '26%', + valeur: '26%', }, 'omr . papier carton . incinération . impact': { - formule: 0.95, + valeur: 0.95, }, } expect(constantFoldingWith(rawRules, ['omr'])).toStrictEqual({ omr: { - valeur: '0.69068kgCO2e', + valeur: '0.69068 kgCO2e', optimized: true, }, }) @@ -367,7 +368,7 @@ describe('Constant folding [base]', () => { it('should replace properly child rule references when one is a substring of the other: (Ambiguity with rule name)', () => { const rawRules = { biogaz: { - formule: + valeur: "biogaz . facteur d'émission * gaz . facteur d'émission + not foldable", }, "biogaz . facteur d'émission": { @@ -382,7 +383,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['biogaz'])).toStrictEqual({ biogaz: { - formule: '(20 * 10) + not foldable', + valeur: '(20 * 10) + not foldable', optimized: true, }, 'not foldable': { @@ -394,7 +395,7 @@ describe('Constant folding [base]', () => { it('replaceAllRefs bug #1', () => { const rawRules = { biogaz: { - formule: + valeur: "gaz . facteur d'émission * biogaz . facteur d'émission + not foldable", }, "biogaz . facteur d'émission": { @@ -409,7 +410,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['biogaz'])).toStrictEqual({ biogaz: { - formule: '(10 * 20) + not foldable', + valeur: '(10 * 20) + not foldable', optimized: true, }, 'not foldable': { @@ -421,7 +422,7 @@ describe('Constant folding [base]', () => { it('replaceAllRefs bug #2', () => { const rawRules = { boisson: { - formule: 'tasse de café * nombre', + valeur: 'tasse de café * nombre', }, 'boisson . tasse de café': { valeur: 20, @@ -432,7 +433,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['boisson'])).toStrictEqual({ boisson: { - formule: '20 * nombre', + valeur: '20 * nombre', optimized: true, }, 'boisson . nombre': { @@ -441,7 +442,7 @@ describe('Constant folding [base]', () => { }) }) - it('should fold standalone [formule] rule', () => { + it('should fold standalone [valeur] rule', () => { const rawRules = { boisson: 'tasse de café * nombre', 'boisson . tasse de café': { @@ -453,7 +454,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['boisson'])).toStrictEqual({ boisson: { - formule: '20 * nombre', + valeur: '20 * nombre', optimized: true, }, 'boisson . nombre': { @@ -466,7 +467,7 @@ describe('Constant folding [base]', () => { const rawRules = { boisson: 'pct * nombre', 'boisson . pct': { - formule: '2%', + valeur: '2%', }, 'boisson . nombre': { 'par défaut': 10, @@ -474,7 +475,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['boisson'])).toStrictEqual({ boisson: { - formule: '2% * nombre', + valeur: '2 % * nombre', optimized: true, }, 'boisson . nombre': { @@ -486,7 +487,7 @@ describe('Constant folding [base]', () => { it('par défaut = 0', () => { const rawRules = { 'chocolat chaud': { - formule: 'tasse de chocolat chaud * nombre', + valeur: 'tasse de chocolat chaud * nombre', }, 'tasse de chocolat chaud': { valeur: 20.3, @@ -498,7 +499,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules, ['chocolat chaud'])).toStrictEqual({ 'chocolat chaud': { - formule: '20.3 * nombre', + valeur: '20.3 * nombre', optimized: true, }, 'chocolat chaud . nombre': { @@ -514,27 +515,27 @@ describe('Constant folding [base]', () => { icônes: '🏠🏊', }, 'piscine . empreinte': { - formule: { somme: ['équipés * nombre * équipés * équipés'] }, + valeur: { somme: ['équipés * nombre * équipés * équipés'] }, }, 'piscine . nombre': { question: 'Combien ?', 'par défaut': 2 }, - 'piscine . équipés': { formule: 45 }, + 'piscine . équipés': { valeur: 45 }, } expect( constantFoldingWith(rawRules, ['piscine . empreinte']), ).toStrictEqual({ 'piscine . empreinte': { - formule: { somme: ['((45 * nombre) * 45) * 45'] }, + somme: ['((45 * nombre) * 45) * 45'], optimized: true, }, 'piscine . nombre': { question: 'Combien ?', 'par défaut': 2 }, }) }) - it('should work with parentheses inside [formule]', () => { + it('should work with parentheses inside [valeur]', () => { const rawRules = { 'divers . ameublement . meubles . armoire . empreinte amortie': { titre: 'Empreinte armoire amortie', - formule: 'armoire . empreinte / (durée * coefficient préservation)', + valeur: 'armoire . empreinte / (durée * coefficient préservation)', unité: 'kgCO2e', }, 'divers . ameublement . meubles . armoire . coefficient préservation': 45, @@ -550,7 +551,7 @@ describe('Constant folding [base]', () => { ).toStrictEqual({ 'divers . ameublement . meubles . armoire . empreinte amortie': { titre: 'Empreinte armoire amortie', - formule: 'armoire . empreinte / (10 * 45)', + valeur: 'armoire . empreinte / (10 * 45)', unité: 'kgCO2e', optimized: true, }, @@ -569,7 +570,7 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - formule: 'constant * 2', + valeur: 'constant * 2', }, constant: { valeur: 10, @@ -583,7 +584,7 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - formule: 'constant * 2', + valeur: 'constant * 2', }, constant: { valeur: 10, @@ -602,10 +603,10 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - formule: 'rule to recompute . nested * 2', + valeur: 'rule to recompute . nested * 2', }, 'rule to recompute . nested': { - formule: 'constant * 4', + valeur: 'constant * 4', }, constant: { valeur: 10, @@ -619,10 +620,10 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - formule: 'rule to recompute . nested * 2', + valeur: 'rule to recompute . nested * 2', }, 'rule to recompute . nested': { - formule: 'constant * 4', + valeur: 'constant * 4', }, constant: { valeur: 10, @@ -641,16 +642,16 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - formule: 'nested 1 * 2', + valeur: 'nested 1 * 2', }, 'rule to recompute . nested 1': { - formule: 'nested 2 * 4', + valeur: 'nested 2 * 4', }, 'rule to recompute . nested 2': { - formule: 'nested 3 * 4', + valeur: 'nested 3 * 4', }, 'rule to recompute . nested 3': { - formule: 'constant * 4', + valeur: 'constant * 4', }, constant: { valeur: 10, @@ -664,16 +665,16 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - formule: 'nested 1 * 2', + valeur: 'nested 1 * 2', }, 'rule to recompute . nested 1': { - formule: 'nested 2 * 4', + valeur: 'nested 2 * 4', }, 'rule to recompute . nested 2': { - formule: 'nested 3 * 4', + valeur: 'nested 3 * 4', }, 'rule to recompute . nested 3': { - formule: 'constant * 4', + valeur: 'constant * 4', }, constant: { valeur: 10, @@ -686,7 +687,7 @@ describe('Constant folding [base]', () => { it('should not fold rules impacted by a [contexte] with nested mechanisms in the formula', () => { const rawRules = { root: { - formule: { + valeur: { somme: ['rule to recompute', 10], }, contexte: { @@ -694,7 +695,7 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - formule: 'constant * 2', + valeur: 'constant * 2', }, constant: { valeur: 10, @@ -702,15 +703,13 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { - formule: { - somme: ['rule to recompute', 10], - }, + somme: ['rule to recompute', 10], contexte: { constant: 20, }, }, 'rule to recompute': { - formule: 'constant * 2', + valeur: 'constant * 2', }, constant: { valeur: 10, @@ -729,10 +728,10 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - formule: 'constant * 2', + valeur: 'constant * 2', }, 'rule to fold': { - formule: 'constant * 4', + valeur: 'constant * 4', }, constant: { valeur: 10, @@ -746,10 +745,10 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - formule: 'constant * 2', + valeur: 'constant * 2', }, 'rule to fold': { - valeur: '40', + valeur: 40, optimized: true, }, constant: { @@ -763,7 +762,7 @@ describe('Constant folding [base]', () => { it('replaceAllRefs bug #3', () => { const rawRules = { boisson: { - formule: 'tasse de café * café', + valeur: 'tasse de café * café', }, 'boisson . café': { valeur: 20, @@ -774,7 +773,7 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules)).toStrictEqual({ boisson: { - formule: 'tasse de café * 20', + valeur: 'tasse de café * 20', optimized: true, }, 'boisson . tasse de café': { @@ -791,11 +790,11 @@ describe('Constant folding [base]', () => { // it('should fold a constant within two degrees with an [applicable si] (set to false) mechanism', () => { // const rawRules = { // A: { - // formule: 'B', + // valeur: 'B', // }, // 'A . B': { // 'applicable si': 'présent', - // formule: 'C * 10', + // valeur: 'C * 10', // }, // 'A . B . présent': { // question: 'Is present?', @@ -807,11 +806,11 @@ describe('Constant folding [base]', () => { // } // expect(constantFoldingWith(rawRules)).toStrictEqual({ // A: { - // formule: 'B', + // valeur: 'B', // }, // 'A . B': { // 'applicable si': 'présent', - // formule: '7 * 10', + // valeur: '7 * 10', // 'est compressée': true, // }, // 'A . B . présent': { @@ -823,11 +822,11 @@ describe('Constant folding [base]', () => { // it('should fold a constant within two degrees with an [applicable si] (set to true) mechanism', () => { // const rawRules = { // A: { - // formule: 'B', + // valeur: 'B', // }, // 'A . B': { // 'applicable si': 'présent', - // formule: 'C * 10', + // valeur: 'C * 10', // }, // 'A . B . présent': { // question: 'Is present?', @@ -839,11 +838,11 @@ describe('Constant folding [base]', () => { // } // expect(constantFoldingWith(rawRules)).toStrictEqual({ // A: { - // formule: 'B', + // valeur: 'B', // }, // 'A . B': { // 'applicable si': 'présent', - // formule: '7 * 10', + // valeur: '7 * 10', // 'est compressée': true, // }, // 'A . B . présent': { @@ -859,7 +858,7 @@ describe('Constant folding [base]', () => { // 'applicable si': { // 'toutes ces conditions': ['unfoldable < foldable'], // }, - // formule: 'foldable * pas foldable', + // valeur: 'foldable * pas foldable', // }, // 'root . foldable': { // valeur: 20, @@ -874,7 +873,7 @@ describe('Constant folding [base]', () => { // // TODO: should be replaced by 'unfoldable < 20' // 'toutes ces conditions': ['unfoldable < foldable'], // }, - // formule: '20 * unfoldable', + // valeur: '20 * unfoldable', // 'est compressée': true, // }, // 'root . unfoldable': { @@ -888,7 +887,7 @@ describe('Constant folding [base]', () => { // 'applicable si': { // 'toutes ces conditions': ['unfoldable > foldable'], // }, - // formule: 'foldable * unfoldable', + // valeur: 'foldable * unfoldable', // }, // 'root . foldable': { // valeur: 20, @@ -902,7 +901,7 @@ describe('Constant folding [base]', () => { // 'applicable si': { // 'toutes ces conditions': ['unfoldable > 20'], // }, - // formule: '20 * unfoldable', + // valeur: '20 * unfoldable', // 'est compressée': true, // }, // 'root . unfoldable': { diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index 127bbbf..f8dd41f 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -703,3 +703,50 @@ describe('API > mecanisms list', () => { // expect(serializedRules).toStrictEqual(rules) // }) }) + +describe('More complexe cases', () => { + it('should serialize the same rules multiple times', () => { + const rules = { + ruleA: { + titre: 'Rule A', + valeur: 'B . C * D', + }, + 'ruleA . B . C': { + valeur: '10', + }, + 'ruleA . D': { + valeur: '3', + }, + } + const parsedRules = new Engine(rules, { + allowOrphanRules: true, + }).getParsedRules() + + expect(serializeParsedRules(parsedRules)).toStrictEqual( + serializeParsedRules(parsedRules), + ) + }) + it('should correctly serialize [valeur] composed with other mecanisms', () => { + const rules = { + ex1: { + valeur: { + somme: ['15.89 €', '12 % * 14 €', '-20 €'], + }, + }, + ex2: { + valeur: '2 * 15.89 €', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual({ + ex1: { + somme: ['15.89 €', '12 % * 14 €', '-20 €'], + }, + ex2: { + valeur: '2 * 15.89 €', + }, + }) + }) +}) From eae39795790b42ba6f7486fb26089818bc350fec Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 18 Jan 2024 16:17:09 +0100 Subject: [PATCH 30/54] refactor(optim): remove unused code --- source/commons.ts | 36 +--------------- source/optims/constantFolding.ts | 44 +------------------ test/getRawRules.test.ts | 74 -------------------------------- 3 files changed, 2 insertions(+), 152 deletions(-) delete mode 100644 test/getRawRules.test.ts diff --git a/source/commons.ts b/source/commons.ts index 4f144bc..238e00f 100644 --- a/source/commons.ts +++ b/source/commons.ts @@ -1,13 +1,5 @@ import { basename } from 'path' -import { - Rule, - ParsedRules, - Logger, - ExprAST, - RuleNode, - reduceAST, - ASTNode, -} from 'publicodes' +import { Rule, Logger, ExprAST, reduceAST, ASTNode } from 'publicodes' import yaml from 'yaml' /** @@ -74,32 +66,6 @@ export type RawRule = Omit | ImportMacro */ export type RawRules = Record -/** - * Returns the raw nodes of a parsed rules object. - * - * @param parsedRules - The parsed rules object. - * - * @returns The raw nodes of the parsed rules object. - */ -export function getRawNodes(parsedRules: ParsedRules): RawRules { - return Object.fromEntries( - Object.values(parsedRules).reduce((acc, rule) => { - const { nom, ...rawNode } = rule.rawNode - // We don't want to keep the `avec` attribute in the raw node - // as they are already resolved in the [parsedRules] object. - delete rawNode['avec'] - - acc.push([ - rule.dottedName, - // If the rule only contained the 'nom' attribute, we don't want to - // keep an empty object in the raw node. - Object.keys(rawNode).length === 0 ? null : rawNode, - ]) - return acc - }, []), - ) as RawRules -} - function consumeMsg(_: string): void {} export const disabledLogger: Logger = { diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 18dc4b6..256a2b5 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -1,18 +1,12 @@ import Engine, { reduceAST, ParsedRules, - parseExpression, transformAST, traverseASTNode, Unit, } from 'publicodes' import type { RuleNode, ASTNode } from 'publicodes' -import { - getAllRefsInNode, - RuleName, - serializeParsedExprAST, - substituteInParsedExpr, -} from '../commons' +import { getAllRefsInNode, RuleName } from '../commons' type RefMap = Map< RuleName, @@ -191,21 +185,6 @@ function isEmptyRule(rule: RuleNode): boolean { return Object.keys(rule.rawNode).length === 0 } -function replaceAllRefs( - str: string, - refName: string, - constantValue: any, - currentRuleName: string, -): string { - const parsedExpression = parseExpression(str, currentRuleName) - const newParsedExpression = substituteInParsedExpr( - parsedExpression, - refName, - constantValue, - ) - return serializeParsedExprAST(newParsedExpression) -} - function lexicalSubstitutionOfRefValue( parent: RuleNode, constant: RuleNode, @@ -301,27 +280,6 @@ function isAlreadyFolded(params: FoldingParams, rule: RuleNode): boolean { return 'rawNode' in rule && params.isFoldedAttr in rule.rawNode } -/** - * Subsitutes [parentRuleNode.formule] ref constant from [refs]. - * - * @note It folds child rules in [refs] if possible. - */ -function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: RuleName[]) { - 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 removeInMap(map: Map, key: K, val: V) { if (map.has(key)) { map.set( diff --git a/test/getRawRules.test.ts b/test/getRawRules.test.ts deleted file mode 100644 index ad9811e..0000000 --- a/test/getRawRules.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { RawRules } from '../source/commons' -import { getRawNodes } from '../source/commons' - -import { callWithParsedRules } from './utils.test' - -function getRawNodesWith(rawRules: any): RawRules { - return callWithParsedRules(getRawNodes, rawRules) -} - -describe('getRawRules', () => { - it('∅ -> ∅', () => { - expect(getRawNodesWith({})).toStrictEqual({}) - }) - it('Single null rule', () => { - expect(getRawNodesWith({ test1: null })).toStrictEqual({ - test1: null, - }) - }) - it('Simple single rule', () => { - const rawRules = { - test2: { - titre: 'Test 2', - formule: '10 * 3', - }, - } - expect(getRawNodesWith(rawRules)).toStrictEqual(rawRules) - }) - it('Number constant', () => { - expect(getRawNodesWith({ test3: 10 })).toStrictEqual({ - test3: { valeur: '10' }, - }) // will be reparsed by the website client, so not a problem? - }) - - // FIXME: doesn't pass with bun but passes with jest. The values seem to be equal. - it('Referenced rules', () => { - const rawRules = { - ruleA: { - titre: 'Rule A', - formule: 'B . C * 3', - }, - 'ruleA . B': null, - 'ruleA . B . C': { - valeur: '10', - }, - } - expect(getRawNodesWith(rawRules)).toStrictEqual(rawRules) - }) - it('Mechansim [avec] should not create empty objects', () => { - const rawRules = { - ruleA: { - avec: { - bus: null, - }, - titre: 'Rule A', - formule: 'B . C * 3', - }, - 'ruleA . B': null, - 'ruleA . B . C': { - valeur: '10', - }, - } - expect(getRawNodesWith(rawRules)).toStrictEqual({ - ruleA: { - titre: 'Rule A', - formule: 'B . C * 3', - }, - 'ruleA . B': null, - 'ruleA . B . C': { - valeur: '10', - }, - 'ruleA . bus': null, - }) - }) -}) From 896962f50061ba51a55d5c5fff958f47a5e46e85 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 22 Jan 2024 10:47:50 +0100 Subject: [PATCH 31/54] fix(optim): fold contextes evaluated as constants --- source/optims/constantFolding.ts | 174 ++++++++++++++-------------- source/serializeParsedRules.ts | 2 +- test/optims/constantFolding.test.ts | 82 ++++++------- 3 files changed, 126 insertions(+), 132 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 256a2b5..93a3dfc 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -86,15 +86,20 @@ function initFoldingCtx( reduceAST( (acc: Set, node: ASTNode) => { if (node.nodeKind === 'contexte') { - // Find all rule references impacted by the contexte in the rule node - const impactedRules = getAllRefsInNodeImpactedByContexte( - ruleName, - node, - node.explanation.contexte.map(([ref, _]) => ref.dottedName), - ) - - impactedRules.forEach((rule) => impactedByContexteRules.add(rule)) - impactedByContexteRules.add(ruleName) + const { missingVariables } = engine.evaluateNode(node) + + // We can't fold it + if (Object.keys(missingVariables).length !== 0) { + // Find all rule references impacted by the contexte in the rule node + const impactedRules = getAllRefsInNodeImpactedByContexte( + ruleName, + node, + node.explanation.contexte.map(([ref, _]) => ref.dottedName), + ) + + impactedRules.forEach((rule) => impactedByContexteRules.add(rule)) + impactedByContexteRules.add(ruleName) + } } if ( node.nodeKind === 'reference' && @@ -170,15 +175,7 @@ function isFoldable( const rawNode = rule.rawNode - return !( - contextRules.has(rule.dottedName) || - 'question' in rawNode || - // NOTE(@EmileRolley): I assume that a rule can have a [par défaut] - // attribute without a [question] one. The behavior could be specified. - 'par défaut' in rawNode || - 'applicable si' in rawNode || - 'non applicable si' in rawNode - ) + return !(contextRules.has(rule.dottedName) || 'question' in rawNode) } function isEmptyRule(rule: RuleNode): boolean { @@ -195,61 +192,19 @@ function lexicalSubstitutionOfRefValue( node.nodeKind === 'reference' && node.dottedName === constant.dottedName ) { - // TODO: needs to be extracted in a function checking if a node is a - // constant node. - // @ts-ignore - return constant.explanation.valeur.explanation.alors + if (constant.explanation.valeur.nodeKind === 'condition') { + return constant.explanation.valeur.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}`, + ) + } } }), parent, ) return newNode as RuleNode - // - // const refName = reduceAST((_, node: ASTNode) => {}, '', parent) - // - // const constValue = constant.rawNode.valeur - // - // // NOTE: here we directly replace the [rawNode] as it's what we get back with [getRawNodes] - // // at the end. - // // Instead, we could transform the complete parsed rule and serialize it at the end. - // // - // // If I continue to transform directly the [rawNode] then I can use directly the - // // rules given to the engine and no need to make a deep copy therefore. We simply - // // need to add the dottedName info in the rawRule. - // if ('formule' in parent.rawNode) { - // if (typeof parent.rawNode.formule === 'string') { - // const newFormule = replaceAllRefs( - // parent.rawNode.formule, - // refName, - // constValue, - // constant.dottedName, - // ) - // parent.rawNode.formule = newFormule - // return parent - // } else if ('somme' in parent.rawNode.formule) { - // // TODO: needs to be abstracted - // parent.rawNode.formule.somme = ( - // parent.rawNode.formule.somme as (string | number)[] - // ).map((expr: string | number) => { - // return typeof expr === 'string' - // ? replaceAllRefs(expr, refName, constValue, constant.dottedName) - // : expr - // }) - // return parent - // } - // } - // // When a rule defined as an unique string: 'var * var2', it's parsed as a [valeur] attribute not a [formule]. - // if (typeof parent.rawNode.valeur === 'string') { - // parent.rawNode.formule = replaceAllRefs( - // parent.rawNode.valeur, - // refName, - // constValue, - // constant.dottedName, - // ) - // delete parent.rawNode.valeur - // return parent - // } } /** Replaces all references in parent refs of [ruleName] by its [rule.valeur] */ @@ -324,32 +279,29 @@ function updateRefCounting( } function replaceRuleWithEvaluatedNodeValue( - rule: ASTNode, + rule: RuleNode, nodeValue: number | boolean | string | Record, unit: Unit | undefined, ) { - const constantNode = { + const constantNode: ASTNode = { nodeValue, - type: typeof nodeValue, + type: + typeof nodeValue === 'number' + ? 'number' + : typeof nodeValue === 'boolean' + ? 'boolean' + : typeof nodeValue === 'string' + ? 'string' + : undefined, + nodeKind: 'constant', missingVariables: {}, rawNode: { valeur: nodeValue, }, - fullPrecision: true, isNullable: false, - isActive: true, } - // - // The engine parse all rules into a root condition: - // - // - si: - // est non défini: . $SITUATION - // - alors: - // - sinon: . $SITUATION - // - // @ts-ignore - rule.explanation.valeur.explanation.alors = + const explanationThen: ASTNode = unit !== undefined ? { nodeKind: 'unité', @@ -357,6 +309,53 @@ function replaceRuleWithEvaluatedNodeValue( explanation: constantNode, } : constantNode + + if (rule.explanation.valeur.nodeKind === 'contexte') { + // We remove the contexte as it's now considered as a constant. + rule.explanation.valeur = rule.explanation.valeur.explanation.node + } + + /* + * The engine parse all rules into a root condition: + * + * - si: + * est non défini: . $SITUATION + * - alors: + * - sinon: . $SITUATION + */ + if (rule.explanation.valeur.nodeKind === 'condition') { + rule.explanation.valeur.explanation.alors = explanationThen + } else if ( + rule.explanation.valeur.nodeKind === 'unité' && + rule.explanation.valeur.explanation.nodeKind === 'condition' + ) { + rule.explanation.valeur.explanation.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}`, + ) + } +} + +/** + * Subsitutes [parentRuleNode.formule] ref constant from [refs]. + * + * @note It folds child rules in [refs] if possible. + */ +function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: RuleName[]) { + 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 { @@ -406,11 +405,11 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { return } else { - // // Try to replace internal refs if possible. - // const childs = ctx.refs.childs.get(ruleName) - // if (childs?.length > 0) { - // replaceAllPossibleChildRefs(ctx, childs) - // } + // Try to replace internal refs if possible. + const childs = ctx.refs.childs.get(ruleName) + if (childs?.length > 0) { + replaceAllPossibleChildRefs(ctx, childs) + } } } @@ -437,6 +436,7 @@ export function constantFolding( for (const ruleName in ctx.parsedRules) { const ruleNode = ctx.parsedRules[ruleName] + if ( isFoldable(ruleNode, ctx.impactedByContexteRules) && !isAlreadyFolded(ctx.params, ruleNode) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index f22980d..077e7ba 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -349,7 +349,7 @@ export function serializeParsedRules( * TODO: a way to keep the [avec] mecanism in the rawNode could be investigated but * for now it's not a priority. */ - const syntaxicSugars = ['avec', 'formule', 'valeur'] + const syntaxicSugars = ['avec', 'formule', 'valeur', 'contexte'] const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index bde4e9d..5bea0d2 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -561,7 +561,7 @@ describe('Constant folding [base]', () => { }) }) - it('should not fold rules impacted by a [contexte]', () => { + it('should not fold rules impacted by a [contexte] with a question in dependency', () => { const rawRules = { root: { valeur: 'rule to recompute', @@ -570,7 +570,10 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - valeur: 'constant * 2', + valeur: 'constant * 2 * question', + }, + question: { + question: 'Question ?', }, constant: { valeur: 10, @@ -584,51 +587,40 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - valeur: 'constant * 2', + valeur: '(constant * 2) * question', + }, + question: { + question: 'Question ?', }, constant: { valeur: 10, - // TODO: should be marked as optimized? - // optimized: true, }, }) }) - it('should not fold nested rules impacted by a [contexte]', () => { + it('should fold rules impacted by a [contexte] with multiple contexte rules', () => { const rawRules = { root: { valeur: 'rule to recompute', contexte: { - constant: 20, + constant: 50, + 'constant 2': 100, }, }, 'rule to recompute': { - valeur: 'rule to recompute . nested * 2', - }, - 'rule to recompute . nested': { - valeur: 'constant * 4', + valeur: 'constant * 2 + constant 2', }, constant: { valeur: 10, }, + 'constant 2': { + valeur: 15, + }, } expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { - valeur: 'rule to recompute', - contexte: { - constant: 20, - }, - }, - 'rule to recompute': { - valeur: 'rule to recompute . nested * 2', - }, - 'rule to recompute . nested': { - valeur: 'constant * 4', - }, - constant: { - valeur: 10, - // TODO: should be marked as optimized? - // optimized: true, + valeur: 200, + optimized: true, }, }) }) @@ -651,7 +643,10 @@ describe('Constant folding [base]', () => { valeur: 'nested 3 * 4', }, 'rule to recompute . nested 3': { - valeur: 'constant * 4', + valeur: 'constant * 4 * question', + }, + question: { + question: 'Question ?', }, constant: { valeur: 10, @@ -674,7 +669,10 @@ describe('Constant folding [base]', () => { valeur: 'nested 3 * 4', }, 'rule to recompute . nested 3': { - valeur: 'constant * 4', + valeur: '(constant * 4) * question', + }, + question: { + question: 'Question ?', }, constant: { valeur: 10, @@ -688,7 +686,7 @@ describe('Constant folding [base]', () => { const rawRules = { root: { valeur: { - somme: ['rule to recompute', 10], + somme: ['rule to recompute', 'question', 10], }, contexte: { constant: 20, @@ -697,13 +695,16 @@ describe('Constant folding [base]', () => { 'rule to recompute': { valeur: 'constant * 2', }, + question: { + question: 'Question ?', + }, constant: { valeur: 10, }, } expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { - somme: ['rule to recompute', 10], + somme: ['rule to recompute', 'question', 10], contexte: { constant: 20, }, @@ -711,6 +712,9 @@ describe('Constant folding [base]', () => { 'rule to recompute': { valeur: 'constant * 2', }, + question: { + question: 'Question ?', + }, constant: { valeur: 10, // TODO: should be marked as optimized? @@ -719,12 +723,12 @@ describe('Constant folding [base]', () => { }) }) - it('should not fold rules used with a [contexte] but still fold used constant in other rules', () => { + it('should fold a constant rule even with [contexte]', () => { const rawRules = { root: { valeur: 'rule to recompute', contexte: { - constant: 20, + constant: 15, }, }, 'rule to recompute': { @@ -739,23 +743,13 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { - valeur: 'rule to recompute', - contexte: { - constant: 20, - }, - }, - 'rule to recompute': { - valeur: 'constant * 2', + valeur: 30, + optimized: true, }, 'rule to fold': { valeur: 40, optimized: true, }, - constant: { - valeur: 10, - // TODO: should be marked as optimized? - // optimized: true, - }, }) }) From e5364891e68d3e91967af49250a4624cbc072a2a Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 22 Jan 2024 11:45:40 +0100 Subject: [PATCH 32/54] test(optim): uncomment tests on [applicable si] etc... --- test/optims/constantFolding.test.ts | 271 +++++++++++++++------------- 1 file changed, 144 insertions(+), 127 deletions(-) diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 5bea0d2..8aed450 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -776,131 +776,148 @@ describe('Constant folding [base]', () => { }) }) - // - // - // TODO: not supported yet - // - // - // it('should fold a constant within two degrees with an [applicable si] (set to false) mechanism', () => { - // const rawRules = { - // A: { - // valeur: 'B', - // }, - // 'A . B': { - // 'applicable si': 'présent', - // valeur: 'C * 10', - // }, - // 'A . B . présent': { - // question: 'Is present?', - // 'par défaut': 'non', - // }, - // 'A . B . C': { - // valeur: 7, - // }, - // } - // expect(constantFoldingWith(rawRules)).toStrictEqual({ - // A: { - // valeur: 'B', - // }, - // 'A . B': { - // 'applicable si': 'présent', - // valeur: '7 * 10', - // 'est compressée': true, - // }, - // 'A . B . présent': { - // question: 'Is present?', - // 'par défaut': 'non', - // }, - // }) - // }) - // it('should fold a constant within two degrees with an [applicable si] (set to true) mechanism', () => { - // const rawRules = { - // A: { - // valeur: 'B', - // }, - // 'A . B': { - // 'applicable si': 'présent', - // valeur: 'C * 10', - // }, - // 'A . B . présent': { - // question: 'Is present?', - // 'par défaut': 'oui', - // }, - // 'A . B . C': { - // valeur: 7, - // }, - // } - // expect(constantFoldingWith(rawRules)).toStrictEqual({ - // A: { - // valeur: 'B', - // }, - // 'A . B': { - // 'applicable si': 'présent', - // valeur: '7 * 10', - // 'est compressée': true, - // }, - // 'A . B . présent': { - // question: 'Is present?', - // 'par défaut': 'oui', - // }, - // }) - // }) - // - // it('should not delete leaf used in [applicable si > toutes ces conditions (evaluated to ⊤)]', () => { - // const rawRules = { - // root: { - // 'applicable si': { - // 'toutes ces conditions': ['unfoldable < foldable'], - // }, - // valeur: 'foldable * pas foldable', - // }, - // 'root . foldable': { - // valeur: 20, - // }, - // 'root . unfoldable': { - // 'par défaut': 10, - // }, - // } - // expect(constantFoldingWith(rawRules)).toStrictEqual({ - // root: { - // 'applicable si': { - // // TODO: should be replaced by 'unfoldable < 20' - // 'toutes ces conditions': ['unfoldable < foldable'], - // }, - // valeur: '20 * unfoldable', - // 'est compressée': true, - // }, - // 'root . unfoldable': { - // 'par défaut': 10, - // }, - // }) - // }) - // it('should not delete leaf used in [applicable si > toutes ces conditions (evaluated to ⊥)] ', () => { - // const rawRules = { - // root: { - // 'applicable si': { - // 'toutes ces conditions': ['unfoldable > foldable'], - // }, - // valeur: 'foldable * unfoldable', - // }, - // 'root . foldable': { - // valeur: 20, - // }, - // 'root . unfoldable': { - // 'par défaut': 10, - // }, - // } - // expect(constantFoldingWith(rawRules)).toStrictEqual({ - // root: { - // 'applicable si': { - // 'toutes ces conditions': ['unfoldable > 20'], - // }, - // valeur: '20 * unfoldable', - // 'est compressée': true, - // }, - // 'root . unfoldable': { - // 'par défaut': 10, - // }, - // }) - // }) + it('should fold a unit rule with a constant [unité]', () => { + const rawRules = { + root: { + formule: '14 repas/semaine * plats . végétalien . empreinte', + unité: 'kgCO2e/semaine', + }, + 'plats . végétalien . empreinte': { + titre: "Empreinte d'un repas végétalien", + formule: 0.785, + unité: 'kgCO2e/repas', + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + root: { + valeur: '10.99 kgCO2e/semaine', + unité: 'kgCO2e/semaine', + optimized: true, + }, + }) + }) + + it('should fold a constant within two degrees with an [applicable si] (set to false) mechanism', () => { + const rawRules = { + A: { + valeur: 'B', + }, + 'A . B': { + 'applicable si': 'présent', + valeur: 'C * 10', + }, + 'A . B . présent': { + question: 'Is present?', + 'par défaut': 'non', + }, + 'A . B . C': { + valeur: 7, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + A: { + valeur: 'B', + }, + 'A . B': { + 'applicable si': 'présent', + valeur: '7 * 10', + optimized: true, + }, + 'A . B . présent': { + question: 'Is present?', + 'par défaut': 'non', + }, + }) + }) + + it('should fold a constant within two degrees with an [applicable si] (set to true) mechanism', () => { + const rawRules = { + A: { + valeur: 'B', + }, + 'A . B': { + 'applicable si': 'présent', + valeur: 'C * 10', + }, + 'A . B . présent': { + question: 'Is present?', + 'par défaut': 'oui', + }, + 'A . B . C': { + valeur: 7, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + A: { + valeur: 'B', + }, + 'A . B': { + 'applicable si': 'présent', + valeur: '7 * 10', + optimized: true, + }, + 'A . B . présent': { + question: 'Is present?', + 'par défaut': 'oui', + }, + }) + }) + + it('should not delete leaf used in [applicable si > toutes ces conditions (evaluated to ⊤)]', () => { + const rawRules = { + root: { + 'applicable si': { + 'toutes ces conditions': ['unfoldable < foldable'], + }, + valeur: 'foldable * unfoldable', + }, + 'root . foldable': { + valeur: 20, + }, + 'root . unfoldable': { + 'par défaut': 10, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + root: { + 'applicable si': { + 'toutes ces conditions': ['unfoldable < 20'], + }, + valeur: '20 * unfoldable', + optimized: true, + }, + 'root . unfoldable': { + 'par défaut': 10, + }, + }) + }) + + it('should not delete leaf used in [applicable si > toutes ces conditions (evaluated to ⊥)] ', () => { + const rawRules = { + root: { + 'applicable si': { + 'toutes ces conditions': ['unfoldable > foldable'], + }, + valeur: 'foldable * unfoldable', + }, + 'root . foldable': { + valeur: 20, + }, + 'root . unfoldable': { + 'par défaut': 10, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + root: { + 'applicable si': { + 'toutes ces conditions': ['unfoldable > 20'], + }, + valeur: '20 * unfoldable', + optimized: true, + }, + 'root . unfoldable': { + 'par défaut': 10, + }, + }) + }) }) From 117f485b239cce3f91e708b8abd67c085be9fc38 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 22 Jan 2024 11:53:44 +0100 Subject: [PATCH 33/54] perf(optim): use Set instead of Array in RefMap --- source/optims/constantFolding.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 93a3dfc..f206bf8 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -11,7 +11,7 @@ import { getAllRefsInNode, RuleName } from '../commons' type RefMap = Map< RuleName, // NOTE: It's an array but it's built from a Set, so no duplication - RuleName[] | undefined + Set | undefined > type RefMaps = { @@ -60,9 +60,9 @@ type FoldingCtx = { function addMapEntry(map: RefMap, key: RuleName, values: RuleName[]) { let vals = map.get(key) if (vals) { - vals = vals.concat(values) + values.forEach((val) => vals.add(val)) } - map.set(key, vals || values) + map.set(key, vals || new Set(values)) } function initFoldingCtx( @@ -235,12 +235,9 @@ function isAlreadyFolded(params: FoldingParams, rule: RuleNode): boolean { return 'rawNode' in rule && params.isFoldedAttr in rule.rawNode } -function removeInMap(map: Map, key: K, val: V) { +function removeInMap(map: Map>, key: K, val: V) { if (map.has(key)) { - map.set( - key, - map.get(key).filter((v) => v !== val), - ) + map.get(key).delete(val) } } @@ -268,11 +265,11 @@ function deleteRule(ctx: FoldingCtx, dottedName: RuleName): void { function updateRefCounting( ctx: FoldingCtx, parentRuleName: RuleName, - ruleNamesToUpdate: RuleName[], + ruleNamesToUpdate: Set, ) { for (const ruleNameToUpdate of ruleNamesToUpdate) { removeInMap(ctx.refs.parents, ruleNameToUpdate, parentRuleName) - if (ctx.refs.parents.get(ruleNameToUpdate)?.length === 0) { + if (ctx.refs.parents.get(ruleNameToUpdate)?.size === 0) { deleteRule(ctx, ruleNameToUpdate) } } @@ -342,7 +339,7 @@ function replaceRuleWithEvaluatedNodeValue( * * @note It folds child rules in [refs] if possible. */ -function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: RuleName[]) { +function replaceAllPossibleChildRefs(ctx: FoldingCtx, refs: Set) { if (refs) { for (const childName of refs) { const childNode = ctx.parsedRules[childName] @@ -372,7 +369,7 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { const ruleParents = ctx.refs.parents.get(ruleName) if ( isEmptyRule(rule) && - (ruleParents === undefined || ruleParents?.length === 0) + (ruleParents === undefined || ruleParents?.size === 0) ) { // Empty rule with no parent deleteRule(ctx, ruleName) @@ -392,12 +389,12 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { return } - const childs = ctx.refs.childs.get(ruleName) ?? [] + const childs = ctx.refs.childs.get(ruleName) ?? new Set() updateRefCounting(ctx, ruleName, childs) delete ctx.parsedRules[ruleName].rawNode.formule - if (ctx.refs.parents.get(ruleName)?.length === 0) { + if (ctx.refs.parents.get(ruleName)?.size === 0) { deleteRule(ctx, ruleName) } else { ctx.parsedRules[ruleName].rawNode[ctx.params.isFoldedAttr] = true @@ -407,7 +404,7 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { } else { // Try to replace internal refs if possible. const childs = ctx.refs.childs.get(ruleName) - if (childs?.length > 0) { + if (childs?.size > 0) { replaceAllPossibleChildRefs(ctx, childs) } } @@ -452,7 +449,7 @@ export function constantFolding( return ( !isFoldable(ruleNode, ctx.impactedByContexteRules) || toKeep([ruleName, ruleNode]) || - parents?.length > 0 + parents?.size > 0 ) }), ) From 2be276e7cd2bb6710d22dae1f5523c6558cc408c Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 22 Jan 2024 13:16:02 +0100 Subject: [PATCH 34/54] fix(optim): use 'fully' and 'partially' marks --- source/optims/constantFolding.ts | 109 +++++++++++++++------------- test/optims/constantFolding.test.ts | 60 +++++++-------- 2 files changed, 87 insertions(+), 82 deletions(-) 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, From 08adb09f6b997c7c7a2bf3288e230d4088ed7d3c Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 22 Jan 2024 15:02:54 +0100 Subject: [PATCH 35/54] fix(optim): manage null values --- source/optims/constantFolding.ts | 99 ++++++++++++----------------- source/serializeParsedRules.ts | 30 +++++++-- test/optims/constantFolding.test.ts | 43 +++++++++++++ 3 files changed, 111 insertions(+), 61 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 1168420..6105937 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -6,7 +6,7 @@ import Engine, { Unit, } from 'publicodes' import type { RuleNode, ASTNode } from 'publicodes' -import { getAllRefsInNode, RuleName } from '../commons' +import { RuleName } from '../commons' type RefMap = Map< RuleName, @@ -91,14 +91,9 @@ function initFoldingCtx( // We can't fold it if (Object.keys(missingVariables).length !== 0) { - // Find all rule references impacted by the contexte in the rule node - const impactedRules = getAllRefsInNodeImpactedByContexte( - ruleName, - node, - node.explanation.contexte.map(([ref, _]) => ref.dottedName), - ) - - impactedRules.forEach((rule) => impactedByContexteRules.add(rule)) + node.explanation.contexte.forEach(([ref, _]) => { + impactedByContexteRules.add(ref.dottedName) + }) impactedByContexteRules.add(ruleName) } } @@ -126,20 +121,6 @@ function initFoldingCtx( } } - // All childs of a rule impacted by a contexte rule are also impacted. - // - // NOTE(@EmileRolley): contexte rule will be added in the contextRules set. - // Therefore, they won't be marked as folded. It's a wanted behavior? Not sure. - // - // WARN(@EmileRolley): the [impactedByContexteRules] is updated while - // iterating it's convenient but the semantics may vary depending on the - // javascript runtime used. - for (const ruleName of impactedByContexteRules) { - refs.childs - .get(ruleName) - ?.forEach((rule) => impactedByContexteRules.add(rule)) - } - return { engine, parsedRules, @@ -152,33 +133,23 @@ function initFoldingCtx( } } -function getAllRefsInNodeImpactedByContexte( - ruleName: RuleName, - node: ASTNode, - contexteRefs: RuleName[], -): RuleName[] { - const impactedRules = getAllRefsInNode(node).filter((ref) => { - return ( - ref !== ruleName && - !ref.endsWith(' . $SITUATION') && - !contexteRefs.includes(ref) - ) - }) - - return impactedRules -} - function isFoldable( rule: RuleNode | undefined, + childs: Set | undefined, contextRules: Set, ): boolean { - if (!rule) { - return false - } + let childInContext = false - const rawNode = rule.rawNode + childs?.forEach((child) => { + if (contextRules.has(child)) { + childInContext = true + return + } + }) - return !(contextRules.has(rule.dottedName) || 'question' in rawNode) + return ( + rule !== undefined && !contextRules.has(rule.dottedName) && !childInContext + ) } function isEmptyRule(rule: RuleNode): boolean { @@ -227,7 +198,13 @@ function searchAndReplaceConstantValueInParentRefs( for (const parentName of refs) { const parentRule = ctx.parsedRules[parentName] - if (isFoldable(parentRule, ctx.impactedByContexteRules)) { + if ( + isFoldable( + parentRule, + ctx.refs.childs.get(parentName), + ctx.impactedByContexteRules, + ) + ) { const newRule = lexicalSubstitutionOfRefValue(parentRule, rule) if (newRule !== undefined) { ctx.parsedRules[parentName] = newRule @@ -264,7 +241,11 @@ function deleteRule(ctx: FoldingCtx, dottedName: RuleName): void { const ruleNode = ctx.parsedRules[dottedName] if ( (ctx.toKeep === undefined || !ctx.toKeep([dottedName, ruleNode])) && - isFoldable(ruleNode, ctx.impactedByContexteRules) + isFoldable( + ruleNode, + ctx.refs.childs.get(dottedName), + ctx.impactedByContexteRules, + ) ) { removeRuleFromRefs(ctx.refs.parents, dottedName) removeRuleFromRefs(ctx.refs.childs, dottedName) @@ -356,7 +337,11 @@ function replaceRuleWithEvaluatedNodeValue( function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { if ( rule !== undefined && - (!isFoldable(rule, ctx.impactedByContexteRules) || + (!isFoldable( + rule, + ctx.refs.childs.get(ruleName), + ctx.impactedByContexteRules, + ) || isAlreadyFolded(ctx.params, rule) || !(ruleName in ctx.parsedRules)) ) { @@ -417,26 +402,25 @@ 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 - console.time(`fold`) while (nbRules !== nbRulesBefore) { for (const ruleName in ctx.parsedRules) { const ruleNode = ctx.parsedRules[ruleName] if ( - isFoldable(ruleNode, ctx.impactedByContexteRules) && + isFoldable( + ruleNode, + ctx.refs.childs.get(ruleName), + ctx.impactedByContexteRules, + ) && !isAlreadyFolded(ctx.params, ruleNode) ) { fold(ctx, ruleName, ruleNode) @@ -445,23 +429,24 @@ export function constantFolding( nbRulesBefore = nbRules nbRules = Object.keys(ctx.parsedRules).length } - console.timeEnd(`fold`) if (toKeep) { - 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) && + isFoldable( + ruleNode, + ctx.refs.childs.get(ruleName), + ctx.impactedByContexteRules, + ) && !toKeep([ruleName, ruleNode]) && (!parents || parents?.size === 0) ) { delete ctx.parsedRules[ruleName] } } - console.timeEnd('filter') } return ctx.parsedRules diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 077e7ba..bd2906d 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -1,7 +1,7 @@ import { ASTNode, ParsedRules, reduceAST, serializeUnit } from 'publicodes' import { RawRule, RuleName } from './commons' -type SerializedRule = RawRule | number | string +type SerializedRule = RawRule | number | string | null function serializedRuleToRawRule(serializedRule: SerializedRule): RawRule { if (typeof serializedRule === 'object') { @@ -26,8 +26,11 @@ function serializeValue(node: ASTNode, needParens = false): SerializedRule { return `'${node.nodeValue}'` case 'number': return Number(node.nodeValue) - // TODO: case 'date': default: { + if (node.nodeValue === null) { + return null + } + // TODO: case 'date': return node.nodeValue?.toLocaleString('fr-FR') } } @@ -77,6 +80,10 @@ function serializeValue(node: ASTNode, needParens = false): SerializedRule { const serializedUnit = serializeUnit(node.unit) const serializedExplanation = serializeASTNode(node.explanation) + if (serializedExplanation === null) { + return null + } + // Inlined unit (e.g. '10 €/mois') if (node?.explanation?.nodeKind === 'constant') { return ( @@ -107,7 +114,7 @@ function serializeSourceMap(node: ASTNode): SerializedRule { const isArray = Array.isArray(value) rawRule[sourceMap.mecanismName] = isArray - ? value.map((v) => serializeASTNode(v)) + ? value.map((v) => serializeASTNode(v)).filter((v) => v !== null) : serializeASTNode(value) } return rawRule @@ -349,7 +356,22 @@ export function serializeParsedRules( * TODO: a way to keep the [avec] mecanism in the rawNode could be investigated but * for now it's not a priority. */ - const syntaxicSugars = ['avec', 'formule', 'valeur', 'contexte'] + const syntaxicSugars = [ + 'avec', + 'formule', + 'valeur', + 'contexte', + 'somme', + 'moyenne', + 'produit', + 'une de ces conditions', + 'toutes ces conditions', + 'est défini', + 'est non défini', + 'texte', + 'le maximum de', + 'le minimum de', + ] const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index e1aaf83..def6f52 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -719,6 +719,49 @@ describe('Constant folding [base]', () => { }) }) + // TODO: fine tune the contexte fold + // it('should 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 * foldable', + // }, + // question: { + // question: 'Question ?', + // }, + // constant: { + // valeur: 10, + // }, + // foldable: { + // valeur: 15, + // }, + // } + // expect(constantFoldingWith(rawRules)).toStrictEqual({ + // root: { + // somme: ['rule to recompute', 'question', 10], + // contexte: { + // constant: 20, + // }, + // }, + // 'rule to recompute': { + // valeur: '(constant * 2) * 15', + // }, + // question: { + // question: 'Question ?', + // }, + // constant: { + // valeur: 10, + // }, + // }) + // }) + it('should fold a constant rule even with [contexte]', () => { const rawRules = { root: { From 1d38f2da7383335bf31b07d8bd1b2788c387e971 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 22 Jan 2024 17:43:32 +0100 Subject: [PATCH 36/54] nitpick(optim): clean --- source/optims/constantFolding.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 6105937..6da40ef 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -8,11 +8,7 @@ import Engine, { import type { RuleNode, ASTNode } from 'publicodes' import { RuleName } from '../commons' -type RefMap = Map< - RuleName, - // NOTE: It's an array but it's built from a Set, so no duplication - Set | undefined -> +type RefMap = Map | undefined> type RefMaps = { parents: RefMap @@ -45,14 +41,9 @@ type FoldingCtx = { * rule4: 20 * ... * ``` - * In this case, [rule2] should not be folded (and all its dependencies - * should not be folded!). - * - * TODO(@EmileRolley): currently, all childs of a rule with a [contexte] - * mechanism are not folded. However, it could be smarter to keep track of - * each contexte rules and fold the child rules that are not impacted by the - * contexte. For now we choose to keep it simple and to over-fold instead of - * taking the risk to alter the result. + * In this case, we don't want to fold [rule2] because it's evaluated with a + * modified situation (unless it's a constant). We also don't want to fold + * [rule3] and [rule4] because they are used in the contexte of [rule]. */ impactedByContexteRules: Set } From 4e5fe75f5621c3806fb8fa4625664f9cd87282c5 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 25 Jan 2024 15:07:25 +0100 Subject: [PATCH 37/54] fix(optim): don't fold nullable rules --- source/optims/constantFolding.ts | 35 ++++++++- test/optims/constantFolding.test.ts | 112 +++++++++++++++------------- 2 files changed, 93 insertions(+), 54 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 6da40ef..e8a666c 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -4,6 +4,7 @@ import Engine, { transformAST, traverseASTNode, Unit, + EvaluatedNode, } from 'publicodes' import type { RuleNode, ASTNode } from 'publicodes' import { RuleName } from '../commons' @@ -325,6 +326,26 @@ function replaceRuleWithEvaluatedNodeValue( } } +function isNullable(node: ASTNode): boolean { + // @ts-ignore + if (node?.explanation?.nullableParent !== undefined) { + return true + } + + return reduceAST( + (_, node) => { + //@ts-ignore + if (node?.explanation?.nullableParent !== undefined) { + return true + } + }, + false, + // We expect a reference node here + // @ts-ignore + node?.explanation?.valeur, + ) +} + function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { if ( rule !== undefined && @@ -350,12 +371,18 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { return } - const { missingVariables, nodeValue, unit } = ctx.engine.evaluateNode(rule) - + const evaluation: ASTNode & EvaluatedNode = ctx.engine.evaluate( + rule.dottedName, + ) + const { missingVariables, nodeValue, unit } = evaluation const missingVariablesNames = Object.keys(missingVariables) - // Constant leaf -> search and replace the constant in all its parents. - if (missingVariablesNames.length === 0) { + if ( + missingVariablesNames.length === 0 && + // We don't want to fold a rule which can be nullable with a different situation. + // For example, if its namespace is conditionnaly applicable. + !isNullable(evaluation) + ) { replaceRuleWithEvaluatedNodeValue(rule, nodeValue, unit) searchAndReplaceConstantValueInParentRefs(ctx, ruleName, rule) diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index def6f52..79e1d51 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -853,20 +853,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ - A: { - valeur: 'B', - }, - 'A . B': { - 'applicable si': 'présent', - valeur: '7 * 10', - optimized: 'partially', - }, - 'A . B . présent': { - question: 'Is present?', - 'par défaut': 'non', - }, - }) + expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) }) it('should fold a constant within two degrees with an [applicable si] (set to true) mechanism', () => { @@ -886,20 +873,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ - A: { - valeur: 'B', - }, - 'A . B': { - 'applicable si': 'présent', - valeur: '7 * 10', - optimized: 'partially', - }, - 'A . B . présent': { - question: 'Is present?', - 'par défaut': 'oui', - }, - }) + expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) }) it('should not delete leaf used in [applicable si > toutes ces conditions (evaluated to ⊤)]', () => { @@ -917,18 +891,7 @@ describe('Constant folding [base]', () => { 'par défaut': 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ - root: { - 'applicable si': { - 'toutes ces conditions': ['unfoldable < 20'], - }, - valeur: '20 * unfoldable', - optimized: 'partially', - }, - 'root . unfoldable': { - 'par défaut': 10, - }, - }) + expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) }) it('should not delete leaf used in [applicable si > toutes ces conditions (evaluated to ⊥)] ', () => { @@ -946,17 +909,66 @@ describe('Constant folding [base]', () => { 'par défaut': 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ - root: { - 'applicable si': { - 'toutes ces conditions': ['unfoldable > 20'], - }, - valeur: '20 * unfoldable', - optimized: 'partially', + expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + }) + + it('should not fold nullable rules evaluated to null in the default situation', () => { + const rawRules = { + A: { + valeur: 'B . C', }, - 'root . unfoldable': { - 'par défaut': 10, + 'A . B': { + 'applicable si': 'présent', + valeur: 'C * 10', }, - }) + 'A . B . présent': { + question: 'Is present?', + 'par défaut': 'non', + }, + // nullable rule + 'A . B . C': { + valeur: 7, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + }) + + it('should not fold nullable rules evaluated not to null in the default situation', () => { + const rawRules = { + A: { + valeur: 'B . C', + }, + 'A . B': { + 'applicable si': 'présent', + valeur: 'C * 10', + }, + 'A . B . présent': { + question: 'Is present?', + 'par défaut': 'oui', + }, + // nullable rule + 'A . B . C': { + valeur: 7, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + + // TODO: fine tune the conditional applicability fold + // { + // A: { + // 'applicable si': 'A . B . présent', + // valeur: 7, + // optimized: 'partially', + // }, + // 'A . B': { + // 'applicable si': 'présent', + // valeur: '7 * 10', + // optimized: 'partially', + // }, + // 'A . B . présent': { + // question: 'Is present?', + // 'par défaut': 'oui', + // }, + // })) }) }) From bcdd9ee9dde6ec4e7dd9af4a189f0b63d3239c41 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 25 Jan 2024 18:53:55 +0100 Subject: [PATCH 38/54] fix(optim): correctly substitute constant value --- source/optims/constantFolding.ts | 103 ++++++++++++------------------- 1 file changed, 41 insertions(+), 62 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index e8a666c..8da2d46 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -148,41 +148,11 @@ function isEmptyRule(rule: RuleNode): boolean { return Object.keys(rule.rawNode).length === 0 } -function lexicalSubstitutionOfRefValue( - parent: RuleNode, - constant: RuleNode, -): RuleNode | undefined { - const newNode = traverseASTNode( - transformAST((node, _) => { - if ( - node.nodeKind === 'reference' && - node.dottedName === constant.dottedName - ) { - 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}`, - ) - } - } - }), - parent, - ) - - return newNode as RuleNode -} - -/** Replaces all references in parent refs of [ruleName] by its [rule.valeur] */ +/** Replaces all references in parent refs of [ruleName] by its [constantNode] */ function searchAndReplaceConstantValueInParentRefs( ctx: FoldingCtx, ruleName: RuleName, - rule: RuleNode, + constantNode: ASTNode, ): void { const refs = ctx.refs.parents.get(ruleName) @@ -197,7 +167,15 @@ function searchAndReplaceConstantValueInParentRefs( ctx.impactedByContexteRules, ) ) { - const newRule = lexicalSubstitutionOfRefValue(parentRule, rule) + const newRule = traverseASTNode( + transformAST((node, _) => { + if (node.nodeKind === 'reference' && node.dottedName === ruleName) { + return constantNode + } + }), + parentRule, + ) as RuleNode + if (newRule !== undefined) { ctx.parsedRules[parentName] = newRule ctx.parsedRules[parentName].rawNode[ctx.params.isFoldedAttr] = @@ -265,7 +243,7 @@ function replaceRuleWithEvaluatedNodeValue( rule: RuleNode, nodeValue: number | boolean | string | Record, unit: Unit | undefined, -) { +): ASTNode { const constantNode: ASTNode = { nodeValue, type: @@ -298,32 +276,24 @@ function replaceRuleWithEvaluatedNodeValue( rule.explanation.valeur = rule.explanation.valeur.explanation.node } - /* - * The engine parse all rules into a root condition: - * - * - si: - * est non défini: . $SITUATION - * - alors: - * - sinon: . $SITUATION - */ - if (rule.explanation.valeur.nodeKind === 'condition') { - rule.explanation.valeur.explanation.alors = explanationThen - } else if ( - rule.explanation.valeur.nodeKind === 'unité' && - 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}`, - ) - } + rule.explanation.valeur = traverseASTNode( + transformAST((node, _) => { + if (node.nodeKind === 'condition') { + /* we found the first condition, which wrapped the rule in the form of: + * + * - si: + * est non défini: . $SITUATION + * - alors: + * - sinon: . $SITUATION + */ + node.explanation.alors = explanationThen + return node + } + }), + rule, + ) + + return explanationThen } function isNullable(node: ASTNode): boolean { @@ -333,7 +303,12 @@ function isNullable(node: ASTNode): boolean { } return reduceAST( + // @ts-ignore (_, node) => { + if (!node) { + return false + } + //@ts-ignore if (node?.explanation?.nullableParent !== undefined) { return true @@ -383,9 +358,13 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { // For example, if its namespace is conditionnaly applicable. !isNullable(evaluation) ) { - replaceRuleWithEvaluatedNodeValue(rule, nodeValue, unit) + const constantNode = replaceRuleWithEvaluatedNodeValue( + rule, + nodeValue, + unit, + ) - searchAndReplaceConstantValueInParentRefs(ctx, ruleName, rule) + searchAndReplaceConstantValueInParentRefs(ctx, ruleName, constantNode) if (ctx.parsedRules[ruleName] === undefined) { return } From 388506311abb718c1eb0cb0bd05d672ecb61b501 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 25 Jan 2024 18:54:14 +0100 Subject: [PATCH 39/54] fix: serialize the [remplacement] mecanism --- source/serializeParsedRules.ts | 48 +++++++++++++-- test/serializeParsedRules.test.ts | 99 +++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 4 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index bd2906d..5a41c9c 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -98,6 +98,10 @@ function serializeValue(node: ASTNode, needParens = false): SerializedRule { } } + case 'variations': { + return serializeASTNode(node) + } + default: { throw new Error(`[SERIALIZE_VALUE]: '${node.nodeKind}' not implemented`) } @@ -124,6 +128,36 @@ function serializeASTNode(node: ASTNode): SerializedRule { return reduceAST( (_, node: ASTNode) => { switch (node?.nodeKind) { + case 'rule': { + const serializedValeur = serializeASTNode(node.explanation.valeur) + + if (node.replacements.length > 0) { + let serializedRemplace = { + 'références à': serializeValue( + node.replacements[0].replacedReference, + ), + } + + if (node.replacements[0].whiteListedNames.length > 0) { + serializedRemplace['dans'] = + node.replacements[0].whiteListedNames.map(({ name }) => name) + } + if (node.replacements[0].blackListedNames.length > 0) { + serializedRemplace['sauf dans'] = + node.replacements[0].blackListedNames.map(({ name }) => name) + } + if (node.replacements[0].priority) { + serializedRemplace['priorité'] = node.replacements[0].priority + } + return { + remplace: serializedRemplace, + ...serializedRuleToRawRule(serializedValeur), + } + } + + return serializedValeur + } + case 'reference': case 'constant': case 'unité': @@ -145,6 +179,11 @@ function serializeASTNode(node: ASTNode): SerializedRule { } case 'variations': { + // If the node is a replacement rule, we need to serialize the original ref + if (node?.sourceMap?.mecanismName === 'replacement') { + // @ts-ignore + return node.sourceMap.args.originalNode.dottedName + } return { variations: node.explanation.map(({ condition, consequence }) => { if ( @@ -288,7 +327,9 @@ function serializeASTNode(node: ASTNode): SerializedRule { default: { throw new Error( - `[SERIALIZE_AST_NODE]: mecanism '${mecanismName}' found in a '${node.nodeKind}`, + `[SERIALIZE_AST_NODE]: mecanism '${mecanismName}' found in a '${ + node.nodeKind + }Node:\n${JSON.stringify(node, null, 2)}`, ) } } @@ -371,6 +412,7 @@ export function serializeParsedRules( 'texte', 'le maximum de', 'le minimum de', + 'remplace', ] const rawRules = {} @@ -381,9 +423,7 @@ export function serializeParsedRules( continue } - const serializedNode = serializedRuleToRawRule( - serializeASTNode(node.explanation.valeur), - ) + const serializedNode = serializedRuleToRawRule(serializeASTNode(node)) rawRules[rule] = { ...node.rawNode } syntaxicSugars.forEach((attr) => { diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index f8dd41f..a3fc26e 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -677,6 +677,105 @@ describe('API > mecanisms list', () => { }) }) + it('should serialize rule with [remplacement]', () => { + const rules = { + 'frais de repas': { + valeur: '5 €/repas', + }, + 'cafés-restaurants': { + valeur: 'oui', + }, + 'cafés-restaurants . frais de repas': { + remplace: { + 'références à': 'frais de repas', + }, + valeur: '6 €/repas', + }, + 'montant repas mensuels': { + valeur: '20 repas * frais de repas', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + + it('should serialize rule with [remplacement] with a shorten rule ref', () => { + const rules = { + 'cafés-restaurants': { + valeur: 'oui', + }, + 'cafés-restaurants . frais de repas': { + remplace: { + 'références à': 'montant repas mensuels . frais de repas', + }, + valeur: '6 €/repas', + }, + 'montant repas mensuels': { + valeur: '20 repas * frais de repas', + }, + 'montant repas mensuels . frais de repas': { + valeur: '5 €/repas', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual({ + ...rules, + 'montant repas mensuels': { + valeur: '20 repas * montant repas mensuels . frais de repas', + }, + }) + }) + + it('should serialize rule with [remplacement] in a specified context', () => { + const rules = { + foo: { + valeur: 0, + }, + 'foo remplacé dans résultat 1': { + remplace: { + 'références à': 'foo', + priorité: 2, + dans: 'résultat 1', + }, + valeur: 2, + }, + 'foo remplacé dans résultat 2': { + remplace: { + 'références à': 'foo', + 'sauf dans': 'résultat 1', + }, + valeur: 3, + }, + 'résultat 1': { valeur: 'foo' }, + 'résultat 2': { valeur: 'foo' }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual({ + ...rules, + 'foo remplacé dans résultat 1': { + remplace: { + 'références à': 'foo', + priorité: 2, + dans: ['résultat 1'], + }, + valeur: 2, + }, + 'foo remplacé dans résultat 2': { + remplace: { + 'références à': 'foo', + 'sauf dans': ['résultat 1'], + }, + valeur: 3, + }, + }) + }) + // TODO // it('should serialize rule with [private rule]', () => { // const rules = { From 911d1f45de74f17fb9bc47e974803abcb863df73 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 25 Jan 2024 19:00:51 +0100 Subject: [PATCH 40/54] fix: serialize the [inversion] mecanism --- source/serializeParsedRules.ts | 7 +++++++ test/serializeParsedRules.test.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 5a41c9c..d9b4988 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -367,6 +367,12 @@ function serializeASTNode(node: ASTNode): SerializedRule { }, } } + case 'inversion': { + return { + 'inversion numérique': + node.explanation.inversionCandidates.map(serializeASTNode), + } + } default: { throw new Error( @@ -413,6 +419,7 @@ export function serializeParsedRules( 'le maximum de', 'le minimum de', 'remplace', + 'par défaut', ] const rawRules = {} diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index a3fc26e..6b9be8f 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -776,6 +776,23 @@ describe('API > mecanisms list', () => { }) }) + it('should serialize rule with [inversion]', () => { + const rules = { + bruts: { + unité: '€/an', + 'par défaut': '0 €/an', + 'inversion numérique': ["nets d'impôt"], + }, + "nets d'impôt": { + valeur: '0 €/an', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + // TODO // it('should serialize rule with [private rule]', () => { // const rules = { From f26243a10cf6f536c0f4786f13f0af3fa87d8587 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 25 Jan 2024 19:27:29 +0100 Subject: [PATCH 41/54] =?UTF-8?q?feat:=20serialize=20the=20[r=C3=A9soudre?= =?UTF-8?q?=20la=20r=C3=A9f=C3=A9rence=20circulaire]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/serializeParsedRules.ts | 27 +++++++++------------------ test/serializeParsedRules.test.ts | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index d9b4988..31e7fd8 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -373,6 +373,14 @@ function serializeASTNode(node: ASTNode): SerializedRule { node.explanation.inversionCandidates.map(serializeASTNode), } } + case 'résoudre référence circulaire': { + return { + 'résoudre la référence circulaire': 'oui', + ...serializedRuleToRawRule( + serializeASTNode(node.explanation.valeur), + ), + } + } default: { throw new Error( @@ -403,24 +411,7 @@ export function serializeParsedRules( * TODO: a way to keep the [avec] mecanism in the rawNode could be investigated but * for now it's not a priority. */ - const syntaxicSugars = [ - 'avec', - 'formule', - 'valeur', - 'contexte', - 'somme', - 'moyenne', - 'produit', - 'une de ces conditions', - 'toutes ces conditions', - 'est défini', - 'est non défini', - 'texte', - 'le maximum de', - 'le minimum de', - 'remplace', - 'par défaut', - ] + const syntaxicSugars = ['avec', 'formule', 'valeur', 'contexte'] const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index 6b9be8f..0142141 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -562,7 +562,7 @@ describe('API > mecanisms list', () => { valeur: '50 €', }, 'prix TTC': { - assiette: 'prix HT * (100 % + TVA)', + valeur: 'prix HT * (100 % + TVA)', }, TVA: { valeur: '50 %', @@ -793,6 +793,19 @@ describe('API > mecanisms list', () => { expect(serializedRules).toStrictEqual(rules) }) + it('should serialize rule with [résoudre la référence circulaire]', () => { + const rules = { + x: { + valeur: '(4 * x) - 5', + 'résoudre la référence circulaire': 'oui', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) + // TODO // it('should serialize rule with [private rule]', () => { // const rules = { From e3eb4f0a41382f4c9d734cada71b551315caef4f Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 1 Feb 2024 11:28:32 +0100 Subject: [PATCH 42/54] fix(optim): don't fold remplacement rules --- source/optims/constantFolding.ts | 40 +++++++++++----------- source/serializeParsedRules.ts | 5 ++- test/optims/constantFolding.test.ts | 53 +++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 8da2d46..0cfd807 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -30,8 +30,8 @@ type FoldingCtx = { toKeep?: PredicateOnRule params: FoldingParams /** - * The rules that are evaluated with a modified situation (in a [contexte] mechanism) - * and we don't want to be folded. + * The rules that are evaluated with a modified situation (in a [contexte] + * mechanism or with a [remplacement]) and we don't want to be folded. * * @example * ``` @@ -46,7 +46,7 @@ type FoldingCtx = { * modified situation (unless it's a constant). We also don't want to fold * [rule3] and [rule4] because they are used in the contexte of [rule]. */ - impactedByContexteRules: Set + unfoldableRules: Set } function addMapEntry(map: RefMap, key: RuleName, values: RuleName[]) { @@ -67,7 +67,7 @@ function initFoldingCtx( parents: new Map(), childs: new Map(), } - const impactedByContexteRules = new Set() + const unfoldableRules = new Set() // 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 @@ -75,6 +75,14 @@ function initFoldingCtx( // rule. for (const ruleName in parsedRules) { const ruleNode = parsedRules[ruleName] + + if (ruleNode.replacements.length > 0) { + unfoldableRules.add(ruleName) + ruleNode.replacements.forEach((replacement) => { + unfoldableRules.add(replacement.replacedReference.name) + }) + } + const reducedAST = reduceAST( (acc: Set, node: ASTNode) => { @@ -83,10 +91,10 @@ function initFoldingCtx( // We can't fold it if (Object.keys(missingVariables).length !== 0) { + unfoldableRules.add(ruleName) node.explanation.contexte.forEach(([ref, _]) => { - impactedByContexteRules.add(ref.dottedName) + unfoldableRules.add(ref.dottedName) }) - impactedByContexteRules.add(ruleName) } } if ( @@ -118,7 +126,7 @@ function initFoldingCtx( parsedRules, refs, toKeep, - impactedByContexteRules, + unfoldableRules, params: { isFoldedAttr: foldingParams?.isFoldedAttr ?? 'optimized', }, @@ -164,7 +172,7 @@ function searchAndReplaceConstantValueInParentRefs( isFoldable( parentRule, ctx.refs.childs.get(parentName), - ctx.impactedByContexteRules, + ctx.unfoldableRules, ) ) { const newRule = traverseASTNode( @@ -211,11 +219,7 @@ function deleteRule(ctx: FoldingCtx, dottedName: RuleName): void { const ruleNode = ctx.parsedRules[dottedName] if ( (ctx.toKeep === undefined || !ctx.toKeep([dottedName, ruleNode])) && - isFoldable( - ruleNode, - ctx.refs.childs.get(dottedName), - ctx.impactedByContexteRules, - ) + isFoldable(ruleNode, ctx.refs.childs.get(dottedName), ctx.unfoldableRules) ) { removeRuleFromRefs(ctx.refs.parents, dottedName) removeRuleFromRefs(ctx.refs.childs, dottedName) @@ -324,11 +328,7 @@ function isNullable(node: ASTNode): boolean { function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { if ( rule !== undefined && - (!isFoldable( - rule, - ctx.refs.childs.get(ruleName), - ctx.impactedByContexteRules, - ) || + (!isFoldable(rule, ctx.refs.childs.get(ruleName), ctx.unfoldableRules) || isAlreadyFolded(ctx.params, rule) || !(ruleName in ctx.parsedRules)) ) { @@ -416,7 +416,7 @@ export function constantFolding( isFoldable( ruleNode, ctx.refs.childs.get(ruleName), - ctx.impactedByContexteRules, + ctx.unfoldableRules, ) && !isAlreadyFolded(ctx.params, ruleNode) ) { @@ -436,7 +436,7 @@ export function constantFolding( isFoldable( ruleNode, ctx.refs.childs.get(ruleName), - ctx.impactedByContexteRules, + ctx.unfoldableRules, ) && !toKeep([ruleName, ruleNode]) && (!parents || parents?.size === 0) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 31e7fd8..b324ea8 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -382,10 +382,9 @@ function serializeASTNode(node: ASTNode): SerializedRule { } } - default: { + case 'replacementRule': { throw new Error( - `[SERIALIZE_AST_NODE]: '${node.nodeKind}' not implemented. - Node:\n${JSON.stringify(node, null, 2)}`, + `[SERIALIZE_AST_NODE]: 'replacementRule' should have been handled before`, ) } } diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 79e1d51..479e881 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -971,4 +971,57 @@ describe('Constant folding [base]', () => { // }, // })) }) + + it('should not fold rules used in a [remplacement]', () => { + const rawRules = { + 'frais de repas': { + valeur: '5 €/repas', + }, + 'cafés-restaurants': { + valeur: 'oui', + }, + 'cafés-restaurants . frais de repas': { + remplace: { + 'références à': 'frais de repas', + }, + valeur: '6 €/repas', + }, + 'montant repas mensuels': { + valeur: '20 repas * frais de repas', + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + ...rawRules, + 'cafés-restaurants': { + valeur: 'oui', + optimized: 'fully', + }, + }) + }) + + it('should not fold rules used in a [remplacement] with a specified context', () => { + const rawRules = { + foo: { + valeur: 0, + }, + 'foo remplacé dans résultat 1': { + remplace: { + 'références à': 'foo', + priorité: 2, + dans: ['résultat 1'], + }, + valeur: 2, + }, + 'foo remplacé dans résultat 2': { + remplace: { + 'références à': 'foo', + 'sauf dans': ['résultat 1'], + }, + valeur: 3, + }, + 'résultat 1': { valeur: 'foo' }, + 'résultat 2': { valeur: 'foo' }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + }) }) From cc5775ec1fccdb94f1f1cb8902aa0d12d38ed361 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 1 Feb 2024 14:24:15 +0100 Subject: [PATCH 43/54] fix: serialize private rules --- source/serializeParsedRules.ts | 9 +++ test/serializeParsedRules.test.ts | 110 +++++++++++++++++++++++------- 2 files changed, 94 insertions(+), 25 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index b324ea8..afadce2 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -414,6 +414,11 @@ export function serializeParsedRules( const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { + if (rule.endsWith(' . $SITUATION')) { + delete rawRules[rule] + continue + } + if (Object.keys(node.rawNode).length === 0) { // Empty rule should be null not {} rawRules[rule] = null @@ -433,6 +438,10 @@ export function serializeParsedRules( ...rawRules[rule], ...serializedNode, } + + if (node.private) { + rawRules[rule]['privé'] = 'oui' + } } return rawRules diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index 0142141..f107a34 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -584,6 +584,13 @@ describe('API > mecanisms list', () => { réduction: '20 %', }, }, + test2: { + valeur: 'a + b', + avec: { + a: null, + b: null, + }, + }, } const serializedRules = serializeParsedRules( new Engine(rules).getParsedRules(), @@ -598,6 +605,11 @@ describe('API > mecanisms list', () => { 'prix final . réduction': { valeur: '20 %', }, + test2: { + valeur: 'a + b', + }, + 'test2 . a': null, + 'test2 . b': null, }) }) @@ -806,31 +818,65 @@ describe('API > mecanisms list', () => { expect(serializedRules).toStrictEqual(rules) }) - // TODO - // it('should serialize rule with [private rule]', () => { - // const rules = { - // 'aide vélo': { - // texte: `La région subventionne l’achat d’un vélo à hauteur de - // {{ prise en charge }} et jusqu’à un plafond de {{ 500 € }}. - // Les éventuelles aides locales déjà perçues sont déduites de - // ce montant. - // - // Par exemple, pour un vélo de {{ exemple }}, la région {{ 'Nouvelle Aquitaine' }} - // vous versera {{ exemple * prise en charge }}`, - // }, - // 'aide vélo . prise en charge': { - // valeur: '50 %', - // }, - // 'aide vélo . exemple': { - // valeur: '250 €', - // }, - // } - // const serializedRules = serializeParsedRules( - // new Engine(rules).getParsedRules(), - // ) - // console.log(JSON.stringify(serializedRules, null, 2)) - // expect(serializedRules).toStrictEqual(rules) - // }) + it('should serialize rule with [private rule]', () => { + const rules = { + assiette: { + valeur: '2100 €', + }, + cotisation: { + produit: ['assiette', 'taux'], + }, + '[privé] taux': { + valeur: '2.8 %', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).baseContext.parsedRules, + ) + expect(serializedRules).toStrictEqual({ + assiette: { + valeur: '2100 €', + }, + cotisation: { + produit: ['assiette', 'taux'], + }, + taux: { + privé: 'oui', + valeur: '2.8 %', + }, + }) + }) + + it('should serialize rule with [private rule] inside [avec]', () => { + const rules = { + assiette: { + valeur: '2100 €', + }, + cotisation: { + produit: ['assiette', 'taux'], + avec: { + '[privé] taux': { + valeur: '2.8 %', + }, + }, + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).baseContext.parsedRules, + ) + expect(serializedRules).toStrictEqual({ + assiette: { + valeur: '2100 €', + }, + cotisation: { + produit: ['assiette', 'taux'], + }, + 'cotisation . taux': { + privé: 'oui', + valeur: '2.8 %', + }, + }) + }) }) describe('More complexe cases', () => { @@ -855,6 +901,7 @@ describe('More complexe cases', () => { serializeParsedRules(parsedRules), ) }) + it('should correctly serialize [valeur] composed with other mecanisms', () => { const rules = { ex1: { @@ -878,4 +925,17 @@ describe('More complexe cases', () => { }, }) }) + + it('should correclty serialize complexe [unité]', () => { + const rules = { + ex1: { + valeur: 10, + unité: '€/part/an', + }, + } + const serializedRules = serializeParsedRules( + new Engine(rules).getParsedRules(), + ) + expect(serializedRules).toStrictEqual(rules) + }) }) From 599d785d88b5e2d563a658bac02369bb5cac317e Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 1 Feb 2024 14:26:39 +0100 Subject: [PATCH 44/54] fix: removed syntaxic sugar from raw node in serialization --- source/serializeParsedRules.ts | 14 ++++++++- test/optims/constantFolding.test.ts | 49 +++++++++++++++++++++++++++++ test/serializeParsedRules.test.ts | 2 +- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index afadce2..11385d4 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -410,7 +410,19 @@ export function serializeParsedRules( * TODO: a way to keep the [avec] mecanism in the rawNode could be investigated but * for now it's not a priority. */ - const syntaxicSugars = ['avec', 'formule', 'valeur', 'contexte'] + const syntaxicSugars = [ + 'avec', + 'formule', + 'valeur', + 'contexte', + 'somme', + 'moyenne', + 'produit', + 'une de ces conditions', + 'toutes ces conditions', + 'est défini', + 'est applicable', + ] const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 479e881..c0c77fd 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -1024,4 +1024,53 @@ describe('Constant folding [base]', () => { } expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) }) + + it('should fully fold a rule with [syntaxic sugar]', () => { + const rawRules = { + foo: { + somme: ['bar', 'baz'], + }, + 'foo 2': { + produit: ['bar', 'baz'], + }, + bar: { + valeur: 10, + }, + baz: { + valeur: 20, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + foo: { + valeur: 30, + optimized: 'fully', + }, + 'foo 2': { + valeur: 200, + optimized: 'fully', + }, + }) + }) + + it('should fold [private rule]', () => { + const rawRules = { + assiette: { + valeur: '2100 €', + }, + cotisation: { + produit: ['assiette', 'taux'], + avec: { + '[privé] taux': { + valeur: '2.8 %', + }, + }, + }, + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + cotisation: { + optimized: 'fully', + valeur: '58.8 €', + }, + }) + }) }) diff --git a/test/serializeParsedRules.test.ts b/test/serializeParsedRules.test.ts index f107a34..b5c7abf 100644 --- a/test/serializeParsedRules.test.ts +++ b/test/serializeParsedRules.test.ts @@ -930,7 +930,7 @@ describe('More complexe cases', () => { const rules = { ex1: { valeur: 10, - unité: '€/part/an', + unité: '€/part.an', }, } const serializedRules = serializeParsedRules( From f4f9f29460dec71b0e73c09d9f7f9d635b2d1c85 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 1 Feb 2024 17:23:54 +0100 Subject: [PATCH 45/54] fix(optim): manage private rules --- source/optims/constantFolding.ts | 77 +++++++++++++++-------------- source/serializeParsedRules.ts | 2 +- test/optims/constantFolding.test.ts | 9 ++++ 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 0cfd807..13ba1fd 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -5,6 +5,7 @@ import Engine, { traverseASTNode, Unit, EvaluatedNode, + utils, } from 'publicodes' import type { RuleNode, ASTNode } from 'publicodes' import { RuleName } from '../commons' @@ -59,7 +60,6 @@ function addMapEntry(map: RefMap, key: RuleName, values: RuleName[]) { function initFoldingCtx( engine: Engine, - parsedRules: ParsedRules, toKeep?: PredicateOnRule, foldingParams?: FoldingParams, ): FoldingCtx { @@ -68,6 +68,10 @@ function initFoldingCtx( childs: new Map(), } const unfoldableRules = new Set() + // // PERF: could it be avoided? + // JSON.parse(JSON.stringify(engine.baseContext.parsedRules)) + + 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 @@ -110,7 +114,7 @@ function initFoldingCtx( ) ?? new Set() const traversedVariables: RuleName[] = Array.from(reducedAST).filter( - (name) => !name.endsWith(' . $SITUATION'), + (name) => !name.endsWith('$SITUATION'), ) if (traversedVariables.length > 0) { @@ -133,22 +137,24 @@ function initFoldingCtx( } } -function isFoldable( - rule: RuleNode | undefined, - childs: Set | undefined, - contextRules: Set, -): boolean { +const unfoldableAttr = ['par défaut', 'question'] + +function isFoldable(ctx: FoldingCtx, rule: RuleNode): boolean { let childInContext = false + const childs = ctx.refs.childs.get(rule.dottedName) childs?.forEach((child) => { - if (contextRules.has(child)) { + if (ctx.unfoldableRules.has(child)) { childInContext = true return } }) return ( - rule !== undefined && !contextRules.has(rule.dottedName) && !childInContext + rule !== undefined && + !unfoldableAttr.find((attr) => attr in rule.rawNode) && + !ctx.unfoldableRules.has(rule.dottedName) && + !childInContext ) } @@ -168,13 +174,7 @@ function searchAndReplaceConstantValueInParentRefs( for (const parentName of refs) { const parentRule = ctx.parsedRules[parentName] - if ( - isFoldable( - parentRule, - ctx.refs.childs.get(parentName), - ctx.unfoldableRules, - ) - ) { + if (isFoldable(ctx, parentRule)) { const newRule = traverseASTNode( transformAST((node, _) => { if (node.nodeKind === 'reference' && node.dottedName === ruleName) { @@ -219,7 +219,7 @@ function deleteRule(ctx: FoldingCtx, dottedName: RuleName): void { const ruleNode = ctx.parsedRules[dottedName] if ( (ctx.toKeep === undefined || !ctx.toKeep([dottedName, ruleNode])) && - isFoldable(ruleNode, ctx.refs.childs.get(dottedName), ctx.unfoldableRules) + isFoldable(ctx, ruleNode) ) { removeRuleFromRefs(ctx.refs.parents, dottedName) removeRuleFromRefs(ctx.refs.childs, dottedName) @@ -328,11 +328,11 @@ function isNullable(node: ASTNode): boolean { function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { if ( rule !== undefined && - (!isFoldable(rule, ctx.refs.childs.get(ruleName), ctx.unfoldableRules) || + (!isFoldable(ctx, rule) || + !utils.isAccessible(ctx.parsedRules, '', rule.dottedName) || isAlreadyFolded(ctx.params, rule) || !(ruleName in ctx.parsedRules)) ) { - // Already managed rule return } @@ -384,6 +384,24 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { } } +/** + * Deep copies the private [parsedRules] field of [engine] (without the '$SITUATION' + * rules). + */ +function copyFullParsedRules(engine: Engine): ParsedRules { + const parsedRules: ParsedRules = {} + + for (const ruleName in engine.baseContext.parsedRules) { + if (!ruleName.endsWith('$SITUATION')) { + parsedRules[ruleName] = structuredClone( + engine.baseContext.parsedRules[ruleName], + ) + } + } + + return parsedRules +} + /** * Applies a constant folding optimisation pass on parsed rules of [engine]. * @@ -399,11 +417,7 @@ export function constantFolding( toKeep?: PredicateOnRule, params?: FoldingParams, ): ParsedRules { - const parsedRules: ParsedRules = - // PERF: could it be avoided? - JSON.parse(JSON.stringify(engine.getParsedRules())) - - let ctx = initFoldingCtx(engine, parsedRules, toKeep, params) + let ctx = initFoldingCtx(engine, toKeep, params) let nbRules = Object.keys(ctx.parsedRules).length let nbRulesBefore = undefined @@ -412,14 +426,7 @@ export function constantFolding( for (const ruleName in ctx.parsedRules) { const ruleNode = ctx.parsedRules[ruleName] - if ( - isFoldable( - ruleNode, - ctx.refs.childs.get(ruleName), - ctx.unfoldableRules, - ) && - !isAlreadyFolded(ctx.params, ruleNode) - ) { + if (isFoldable(ctx, ruleNode) && !isAlreadyFolded(ctx.params, ruleNode)) { fold(ctx, ruleName, ruleNode) } } @@ -433,11 +440,7 @@ export function constantFolding( const parents = ctx.refs.parents.get(ruleName) if ( - isFoldable( - ruleNode, - ctx.refs.childs.get(ruleName), - ctx.unfoldableRules, - ) && + isFoldable(ctx, ruleNode) && !toKeep([ruleName, ruleNode]) && (!parents || parents?.size === 0) ) { diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 11385d4..3da156e 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -426,7 +426,7 @@ export function serializeParsedRules( const rawRules = {} for (const [rule, node] of Object.entries(parsedRules)) { - if (rule.endsWith(' . $SITUATION')) { + if (rule.endsWith('$SITUATION') || rule.includes('$INTERNAL')) { delete rawRules[rule] continue } diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index c0c77fd..2b0a833 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -1013,6 +1013,7 @@ describe('Constant folding [base]', () => { valeur: 2, }, 'foo remplacé dans résultat 2': { + 'applicable si': 'non', remplace: { 'références à': 'foo', 'sauf dans': ['résultat 1'], @@ -1065,12 +1066,20 @@ describe('Constant folding [base]', () => { }, }, }, + foo: { + privé: 'oui', + 'par défaut': 10, + }, } expect(constantFoldingWith(rawRules)).toStrictEqual({ cotisation: { optimized: 'fully', valeur: '58.8 €', }, + foo: { + privé: 'oui', + 'par défaut': 10, + }, }) }) }) From 0fbfe155c06db3dfaef3113782b106cb676a6ffb Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Fri, 2 Feb 2024 12:14:21 +0100 Subject: [PATCH 46/54] fix(optim): correctly mark rules as fully optimized --- source/optims/constantFolding.ts | 19 +++++++++++++------ test/optims/constantFolding.test.ts | 28 +++++++++------------------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 13ba1fd..810e0dc 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -215,8 +215,9 @@ function removeRuleFromRefs(ref: RefMap, ruleName: RuleName) { } } -function deleteRule(ctx: FoldingCtx, dottedName: RuleName): void { +function tryToDeleteRule(ctx: FoldingCtx, dottedName: RuleName): boolean { const ruleNode = ctx.parsedRules[dottedName] + if ( (ctx.toKeep === undefined || !ctx.toKeep([dottedName, ruleNode])) && isFoldable(ctx, ruleNode) @@ -226,7 +227,11 @@ function deleteRule(ctx: FoldingCtx, dottedName: RuleName): void { delete ctx.parsedRules[dottedName] ctx.refs.parents.delete(dottedName) ctx.refs.childs.delete(dottedName) + + return true } + + return false } /** Removes the [parentRuleName] as a parent dependency of each [childRuleNamesToUpdate]. */ @@ -238,7 +243,7 @@ function updateRefCounting( for (const ruleNameToUpdate of ruleNamesToUpdate) { removeInMap(ctx.refs.parents, ruleNameToUpdate, parentRuleName) if (ctx.refs.parents.get(ruleNameToUpdate)?.size === 0) { - deleteRule(ctx, ruleNameToUpdate) + tryToDeleteRule(ctx, ruleNameToUpdate) } } } @@ -342,7 +347,7 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { (ruleParents === undefined || ruleParents?.size === 0) ) { // Empty rule with no parent - deleteRule(ctx, ruleName) + tryToDeleteRule(ctx, ruleName) return } @@ -375,11 +380,13 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { delete ctx.parsedRules[ruleName].rawNode.formule if (ctx.refs.parents.get(ruleName)?.size === 0) { - deleteRule(ctx, ruleName) - } else { - ctx.parsedRules[ruleName].rawNode[ctx.params.isFoldedAttr] = 'fully' + if (tryToDeleteRule(ctx, ruleName)) { + return + } } + ctx.parsedRules[ruleName].rawNode[ctx.params.isFoldedAttr] = 'fully' + return } } diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 2b0a833..a409f62 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -563,23 +563,6 @@ describe('Constant folding [base]', () => { it('should not fold rules impacted by a [contexte] with a question in dependency', () => { const rawRules = { - root: { - valeur: 'rule to recompute', - contexte: { - constant: 20, - }, - }, - 'rule to recompute': { - valeur: 'constant * 2 * question', - }, - question: { - question: 'Question ?', - }, - constant: { - valeur: 10, - }, - } - expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { valeur: 'rule to recompute', contexte: { @@ -595,7 +578,8 @@ describe('Constant folding [base]', () => { constant: { valeur: 10, }, - }) + } + expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) }) it('should fold rules impacted by a [contexte] with multiple contexte rules', () => { @@ -617,11 +601,17 @@ describe('Constant folding [base]', () => { valeur: 15, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect( + constantFoldingWith(rawRules, ['root', 'rule to recompute']), + ).toStrictEqual({ root: { valeur: 200, optimized: 'fully', }, + 'rule to recompute': { + valeur: 35, + optimized: 'fully', + }, }) }) From 2fd6b882449c4b1b76a6ddb7175f08637c491ea4 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Fri, 2 Feb 2024 12:40:11 +0100 Subject: [PATCH 47/54] feat(optim): correctly substitute ref --- source/optims/constantFolding.ts | 43 +++++++------- test/optims/constantFolding.test.ts | 91 ++++++++++------------------- 2 files changed, 54 insertions(+), 80 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 810e0dc..7541baa 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -68,9 +68,6 @@ function initFoldingCtx( childs: new Map(), } const unfoldableRules = new Set() - // // PERF: could it be avoided? - // JSON.parse(JSON.stringify(engine.baseContext.parsedRules)) - const parsedRules = copyFullParsedRules(engine) // NOTE: we need to traverse the AST to find all the references of a rule. @@ -83,6 +80,10 @@ function initFoldingCtx( if (ruleNode.replacements.length > 0) { unfoldableRules.add(ruleName) ruleNode.replacements.forEach((replacement) => { + // TODO: we could use white-listed and black-listed rules to be more + // precise about which rules we want to fold. But for now, we + // consider that all rules that are replaced are not foldable in + // all cases. unfoldableRules.add(replacement.replacedReference.name) }) } @@ -173,23 +174,20 @@ function searchAndReplaceConstantValueInParentRefs( if (refs) { for (const parentName of refs) { const parentRule = ctx.parsedRules[parentName] - - if (isFoldable(ctx, parentRule)) { - const newRule = traverseASTNode( - transformAST((node, _) => { - if (node.nodeKind === 'reference' && node.dottedName === ruleName) { - return constantNode - } - }), - parentRule, - ) as RuleNode - - if (newRule !== undefined) { - ctx.parsedRules[parentName] = newRule - ctx.parsedRules[parentName].rawNode[ctx.params.isFoldedAttr] = - 'partially' - removeInMap(ctx.refs.parents, ruleName, parentName) - } + const newRule = traverseASTNode( + transformAST((node, _) => { + if (node.nodeKind === 'reference' && node.dottedName === ruleName) { + return constantNode + } + }), + parentRule, + ) as RuleNode + + if (newRule !== undefined) { + ctx.parsedRules[parentName] = newRule + ctx.parsedRules[parentName].rawNode[ctx.params.isFoldedAttr] = + 'partially' + removeInMap(ctx.refs.parents, ruleName, parentName) } } } @@ -379,7 +377,10 @@ function fold(ctx: FoldingCtx, ruleName: RuleName, rule: RuleNode): void { updateRefCounting(ctx, ruleName, childs) delete ctx.parsedRules[ruleName].rawNode.formule - if (ctx.refs.parents.get(ruleName)?.size === 0) { + const parents = ctx.refs.parents.get(ruleName) + // NOTE(@EmileRolley): if the rule has no parent ([parents === undefined]) + // we assume it's a root rule and we don't want to delete it. + if (parents !== undefined && parents.size === 0) { if (tryToDeleteRule(ctx, ruleName)) { return } diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index a409f62..78749ff 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -633,7 +633,7 @@ describe('Constant folding [base]', () => { valeur: 'nested 3 * 4', }, 'rule to recompute . nested 3': { - valeur: 'constant * 4 * question', + valeur: '(constant * 4) * question', }, question: { question: 'Question ?', @@ -642,24 +642,38 @@ describe('Constant folding [base]', () => { valeur: 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + }) + + it('should not fold rules impacted by a [contexte] with nested mechanisms in the formula', () => { + const rawRules = { root: { - valeur: 'rule to recompute', + valeur: { + somme: ['rule to recompute', 'question', 10], + }, contexte: { constant: 20, }, }, 'rule to recompute': { - valeur: 'nested 1 * 2', + valeur: 'constant * 2', }, - 'rule to recompute . nested 1': { - valeur: 'nested 2 * 4', + question: { + question: 'Question ?', }, - 'rule to recompute . nested 2': { - valeur: 'nested 3 * 4', + constant: { + valeur: 10, }, - 'rule to recompute . nested 3': { - valeur: '(constant * 4) * question', + } + expect(constantFoldingWith(rawRules)).toStrictEqual({ + root: { + somme: ['rule to recompute', 'question', 10], + contexte: { + constant: 20, + }, + }, + 'rule to recompute': { + valeur: 'constant * 2', }, question: { question: 'Question ?', @@ -670,18 +684,16 @@ describe('Constant folding [base]', () => { }) }) - it('should not fold rules impacted by a [contexte] with nested mechanisms in the formula', () => { + it('should fold rules impacted by a [contexte] with nested mechanisms in the formula', () => { const rawRules = { root: { - valeur: { - somme: ['rule to recompute', 'question', 10], - }, + somme: ['rule to recompute', 'question', 10], contexte: { constant: 20, }, }, 'rule to recompute': { - valeur: 'constant * 2', + valeur: 'constant * 2 * foldable', }, question: { question: 'Question ?', @@ -689,6 +701,9 @@ describe('Constant folding [base]', () => { constant: { valeur: 10, }, + foldable: { + valeur: 15, + }, } expect(constantFoldingWith(rawRules)).toStrictEqual({ root: { @@ -698,7 +713,8 @@ describe('Constant folding [base]', () => { }, }, 'rule to recompute': { - valeur: 'constant * 2', + valeur: '(constant * 2) * 15', + optimized: 'partially', }, question: { question: 'Question ?', @@ -709,49 +725,6 @@ describe('Constant folding [base]', () => { }) }) - // TODO: fine tune the contexte fold - // it('should 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 * foldable', - // }, - // question: { - // question: 'Question ?', - // }, - // constant: { - // valeur: 10, - // }, - // foldable: { - // valeur: 15, - // }, - // } - // expect(constantFoldingWith(rawRules)).toStrictEqual({ - // root: { - // somme: ['rule to recompute', 'question', 10], - // contexte: { - // constant: 20, - // }, - // }, - // 'rule to recompute': { - // valeur: '(constant * 2) * 15', - // }, - // question: { - // question: 'Question ?', - // }, - // constant: { - // valeur: 10, - // }, - // }) - // }) - it('should fold a constant rule even with [contexte]', () => { const rawRules = { root: { From 508bdb1cb2692513ef33a2829c5e823971358400 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Fri, 2 Feb 2024 15:54:02 +0100 Subject: [PATCH 48/54] =?UTF-8?q?nitpick:=20factorize=20[bar=C3=A8me]=20se?= =?UTF-8?q?rialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/serializeParsedRules.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/serializeParsedRules.ts b/source/serializeParsedRules.ts index 3da156e..ccbdb88 100644 --- a/source/serializeParsedRules.ts +++ b/source/serializeParsedRules.ts @@ -244,9 +244,7 @@ function serializeASTNode(node: ASTNode): SerializedRule { ) if (serializedMultiplicateur !== 1) { - serializedNode['multiplicateur'] = serializeASTNode( - node.explanation.multiplicateur, - ) + serializedNode['multiplicateur'] = serializedMultiplicateur } return { [node.nodeKind]: serializedNode } From 8c49e9f7cdd4a4bd91c1d2077bd182d0ea99d489 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Fri, 2 Feb 2024 16:16:46 +0100 Subject: [PATCH 49/54] pkg: constrain the node version --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b7014f2..5f1b5f1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "format": "prettier --write .", "format:check": "prettier --check ." }, + "engines": { + "node": ">=17" + }, "exports": { ".": { "import": "./dist/index.js", @@ -53,10 +56,9 @@ "@types/jest": "^29.2.5", "docdash": "^2.0.1", "jest": "^29.4.1", - "mitata": "^0.1.6", "prettier": "^3.0.0", "ts-jest": "^29.0.4", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsup": "^6.5.0", "typedoc": "^0.24.8", "typedoc-plugin-export-functions": "^1.0.0", From c9e26765a45b34d5296142d170a96512304278d5 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Fri, 2 Feb 2024 16:17:15 +0100 Subject: [PATCH 50/54] test: change 'toStrictEqual' to 'toEqual' due to the use of 'structuredClone' --- test/optims/constantFolding.test.ts | 84 ++++++++++++++--------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index 78749ff..ddffe14 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -16,6 +16,8 @@ function constantFoldingWith(rawRules: any, targets?: RuleName[]): RawRules { return serializeParsedRules(res) } +// NOTE(@EmileRolley): I modified `toStrictEqual` to `toEqual` when using `structuredClone` +// instead of `JSON.parse(JSON.stringify())`. describe('Constant folding [meta]', () => { it('should not modify the original rules', () => { const rawRules = { @@ -44,8 +46,8 @@ describe('Constant folding [meta]', () => { shouldNotBeModifiedRules, ) - expect(baseParsedRules).toStrictEqual(shouldNotBeModifiedRules) - expect(serializedBaseParsedRules).toStrictEqual( + expect(baseParsedRules).toEqual(shouldNotBeModifiedRules) + expect(serializedBaseParsedRules).toEqual( serializedShouldNotBeModifiedRules, ) }) @@ -53,7 +55,7 @@ describe('Constant folding [meta]', () => { describe('Constant folding [base]', () => { it('∅ -> ∅', () => { - expect(constantFoldingWith({})).toStrictEqual({}) + expect(constantFoldingWith({})).toEqual({}) }) it('should remove empty nodes', () => { @@ -63,7 +65,7 @@ describe('Constant folding [base]', () => { valeur: '10 * 10', }, }), - ).toStrictEqual({ + ).toEqual({ ruleB: { valeur: 100, optimized: 'fully', @@ -81,7 +83,7 @@ describe('Constant folding [base]', () => { valeur: '10', }, } - expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['ruleA'])).toEqual({ ruleA: { titre: 'Rule A', valeur: 30, @@ -103,7 +105,7 @@ describe('Constant folding [base]', () => { valeur: '3', }, } - expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['ruleA'])).toEqual({ ruleA: { titre: 'Rule A', valeur: 30, @@ -125,7 +127,7 @@ describe('Constant folding [base]', () => { valeur: '10', }, } - expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['ruleA'])).toEqual({ ruleA: { titre: 'Rule A', valeur: '10 * D', @@ -153,7 +155,7 @@ describe('Constant folding [base]', () => { valeur: '10', }, } - expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['ruleA'])).toEqual({ ruleA: { titre: 'Rule A', valeur: '10 * D', @@ -181,7 +183,7 @@ describe('Constant folding [base]', () => { valeur: '10', }, } - expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['ruleA'])).toEqual({ ruleA: { titre: 'Rule A', valeur: '10 * D', @@ -205,7 +207,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules, ['A'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['A'])).toEqual({ A: { valeur: 70, optimized: 'fully', @@ -231,7 +233,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules, ['B'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['B'])).toEqual({ B: { valeur: '70 * D', optimized: 'partially', @@ -257,7 +259,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['ruleA'])).toEqual({ ruleA: { valeur: 174, optimized: 'fully', @@ -285,7 +287,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules, ['ruleA'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['ruleA'])).toEqual({ ruleA: { valeur: 'ruleB', }, @@ -357,7 +359,7 @@ describe('Constant folding [base]', () => { valeur: 0.95, }, } - expect(constantFoldingWith(rawRules, ['omr'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['omr'])).toEqual({ omr: { valeur: '0.69068 kgCO2e', optimized: 'fully', @@ -381,7 +383,7 @@ describe('Constant folding [base]', () => { question: 'The user needs to provide a value.', }, } - expect(constantFoldingWith(rawRules, ['biogaz'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['biogaz'])).toEqual({ biogaz: { valeur: '(20 * 10) + not foldable', optimized: 'partially', @@ -408,7 +410,7 @@ describe('Constant folding [base]', () => { question: 'The user needs to provide a value.', }, } - expect(constantFoldingWith(rawRules, ['biogaz'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['biogaz'])).toEqual({ biogaz: { valeur: '(10 * 20) + not foldable', optimized: 'partially', @@ -431,7 +433,7 @@ describe('Constant folding [base]', () => { 'par défaut': 10, }, } - expect(constantFoldingWith(rawRules, ['boisson'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['boisson'])).toEqual({ boisson: { valeur: '20 * nombre', optimized: 'partially', @@ -452,7 +454,7 @@ describe('Constant folding [base]', () => { 'par défaut': 10, }, } - expect(constantFoldingWith(rawRules, ['boisson'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['boisson'])).toEqual({ boisson: { valeur: '20 * nombre', optimized: 'partially', @@ -473,7 +475,7 @@ describe('Constant folding [base]', () => { 'par défaut': 10, }, } - expect(constantFoldingWith(rawRules, ['boisson'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['boisson'])).toEqual({ boisson: { valeur: '2 % * nombre', optimized: 'partially', @@ -497,7 +499,7 @@ describe('Constant folding [base]', () => { 'par défaut': 0, }, } - expect(constantFoldingWith(rawRules, ['chocolat chaud'])).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['chocolat chaud'])).toEqual({ 'chocolat chaud': { valeur: '20.3 * nombre', optimized: 'partially', @@ -520,9 +522,7 @@ describe('Constant folding [base]', () => { 'piscine . nombre': { question: 'Combien ?', 'par défaut': 2 }, 'piscine . équipés': { valeur: 45 }, } - expect( - constantFoldingWith(rawRules, ['piscine . empreinte']), - ).toStrictEqual({ + expect(constantFoldingWith(rawRules, ['piscine . empreinte'])).toEqual({ 'piscine . empreinte': { somme: ['((45 * nombre) * 45) * 45'], optimized: 'partially', @@ -548,7 +548,7 @@ describe('Constant folding [base]', () => { constantFoldingWith(rawRules, [ 'divers . ameublement . meubles . armoire . empreinte amortie', ]), - ).toStrictEqual({ + ).toEqual({ 'divers . ameublement . meubles . armoire . empreinte amortie': { titre: 'Empreinte armoire amortie', valeur: 'armoire . empreinte / (10 * 45)', @@ -579,7 +579,7 @@ describe('Constant folding [base]', () => { valeur: 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + expect(constantFoldingWith(rawRules)).toEqual(rawRules) }) it('should fold rules impacted by a [contexte] with multiple contexte rules', () => { @@ -603,7 +603,7 @@ describe('Constant folding [base]', () => { } expect( constantFoldingWith(rawRules, ['root', 'rule to recompute']), - ).toStrictEqual({ + ).toEqual({ root: { valeur: 200, optimized: 'fully', @@ -642,7 +642,7 @@ describe('Constant folding [base]', () => { valeur: 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + expect(constantFoldingWith(rawRules)).toEqual(rawRules) }) it('should not fold rules impacted by a [contexte] with nested mechanisms in the formula', () => { @@ -665,7 +665,7 @@ describe('Constant folding [base]', () => { valeur: 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect(constantFoldingWith(rawRules)).toEqual({ root: { somme: ['rule to recompute', 'question', 10], contexte: { @@ -705,7 +705,7 @@ describe('Constant folding [base]', () => { valeur: 15, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect(constantFoldingWith(rawRules)).toEqual({ root: { somme: ['rule to recompute', 'question', 10], contexte: { @@ -743,7 +743,7 @@ describe('Constant folding [base]', () => { valeur: 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect(constantFoldingWith(rawRules)).toEqual({ root: { valeur: 30, optimized: 'fully', @@ -767,7 +767,7 @@ describe('Constant folding [base]', () => { question: '?', }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect(constantFoldingWith(rawRules)).toEqual({ boisson: { valeur: 'tasse de café * 20', optimized: 'partially', @@ -790,7 +790,7 @@ describe('Constant folding [base]', () => { unité: 'kgCO2e/repas', }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect(constantFoldingWith(rawRules)).toEqual({ root: { valeur: '10.99 kgCO2e/semaine', unité: 'kgCO2e/semaine', @@ -816,7 +816,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + expect(constantFoldingWith(rawRules)).toEqual(rawRules) }) it('should fold a constant within two degrees with an [applicable si] (set to true) mechanism', () => { @@ -836,7 +836,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + expect(constantFoldingWith(rawRules)).toEqual(rawRules) }) it('should not delete leaf used in [applicable si > toutes ces conditions (evaluated to ⊤)]', () => { @@ -854,7 +854,7 @@ describe('Constant folding [base]', () => { 'par défaut': 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + expect(constantFoldingWith(rawRules)).toEqual(rawRules) }) it('should not delete leaf used in [applicable si > toutes ces conditions (evaluated to ⊥)] ', () => { @@ -872,7 +872,7 @@ describe('Constant folding [base]', () => { 'par défaut': 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + expect(constantFoldingWith(rawRules)).toEqual(rawRules) }) it('should not fold nullable rules evaluated to null in the default situation', () => { @@ -893,7 +893,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + expect(constantFoldingWith(rawRules)).toEqual(rawRules) }) it('should not fold nullable rules evaluated not to null in the default situation', () => { @@ -914,7 +914,7 @@ describe('Constant folding [base]', () => { valeur: 7, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + expect(constantFoldingWith(rawRules)).toEqual(rawRules) // TODO: fine tune the conditional applicability fold // { @@ -953,7 +953,7 @@ describe('Constant folding [base]', () => { valeur: '20 repas * frais de repas', }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect(constantFoldingWith(rawRules)).toEqual({ ...rawRules, 'cafés-restaurants': { valeur: 'oui', @@ -986,7 +986,7 @@ describe('Constant folding [base]', () => { 'résultat 1': { valeur: 'foo' }, 'résultat 2': { valeur: 'foo' }, } - expect(constantFoldingWith(rawRules)).toStrictEqual(rawRules) + expect(constantFoldingWith(rawRules)).toEqual({ ...rawRules }) }) it('should fully fold a rule with [syntaxic sugar]', () => { @@ -1004,7 +1004,7 @@ describe('Constant folding [base]', () => { valeur: 20, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect(constantFoldingWith(rawRules)).toEqual({ foo: { valeur: 30, optimized: 'fully', @@ -1034,7 +1034,7 @@ describe('Constant folding [base]', () => { 'par défaut': 10, }, } - expect(constantFoldingWith(rawRules)).toStrictEqual({ + expect(constantFoldingWith(rawRules)).toEqual({ cotisation: { optimized: 'fully', valeur: '58.8 €', From 7ae9db1c81d068223c063b71f652e41f5137b135 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Fri, 2 Feb 2024 18:30:44 +0100 Subject: [PATCH 51/54] fix(optim): better handling of contexte rules --- source/optims/constantFolding.ts | 80 +++++++++++++------------ test/optims/constantFolding.test.ts | 93 ++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 58 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 7541baa..1e55f63 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -50,12 +50,10 @@ type FoldingCtx = { unfoldableRules: Set } -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) { + const vals = map.get(key) ?? new Set() + values.forEach((val) => vals.add(val)) + map.set(key, vals) } function initFoldingCtx( @@ -70,10 +68,6 @@ function initFoldingCtx( const unfoldableRules = new Set() 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] @@ -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 = + // 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, 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) } @@ -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])) }) } } @@ -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) { @@ -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() diff --git a/test/optims/constantFolding.test.ts b/test/optims/constantFolding.test.ts index ddffe14..d6fc4a4 100644 --- a/test/optims/constantFolding.test.ts +++ b/test/optims/constantFolding.test.ts @@ -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', @@ -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: { @@ -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', () => { @@ -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: { From 46e9cb1873d72f635ad2f9c7ad0b44921fc17453 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Fri, 2 Feb 2024 18:32:45 +0100 Subject: [PATCH 52/54] fix(ci): remove the fixed node version --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5802e7..366ce02 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,6 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16 cache: yarn - run: yarn install --immutable From a2a0d54f28b9e770631a4d7b3d973bdee8d5c14f Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 12 Feb 2024 10:42:38 +0100 Subject: [PATCH 53/54] pkg: upgrade to publicodes@v1.0.1 --- package.json | 2 +- yarn.lock | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 5f1b5f1..8eb917b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "license": "MIT", "dependencies": { "@types/node": "^18.11.18", - "publicodes": "1.0.0-beta.77" + "publicodes": "^1.0.1" }, "devDependencies": { "@types/jest": "^29.2.5", diff --git a/yarn.lock b/yarn.lock index 8c63f98..7768604 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2221,11 +2221,6 @@ minimatch@^9.0.0, minimatch@^9.0.1: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== -mitata@^0.1.6: - version "0.1.8" - resolved "https://registry.yarnpkg.com/mitata/-/mitata-0.1.8.tgz#bc000a5945b7f977567089ce55d4a29cf6e31f42" - integrity sha512-q+tTmAMdyT69SfwvOkMMCCzLhqoPY9OmbZ2nTDwFopTYMKl9HLRA+XfDXydGaRzVEYehkyfP1pFYoXzE6jVRKw== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -2407,10 +2402,10 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -publicodes@1.0.0-beta.77: - version "1.0.0-beta.77" - resolved "https://registry.yarnpkg.com/publicodes/-/publicodes-1.0.0-beta.77.tgz#45e3c4d2a46bfadcc932e1405ea037e659c28134" - integrity sha512-F8U3WGUWMo3/rxhWYS1gWIiG20g1Yy/+PpXdHM99d6ZHKWnnyh/4txVEuyVE75glgDs+mTjwZPnmoKWsTMXluA== +publicodes@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/publicodes/-/publicodes-1.0.1.tgz#07c1e5d94b3609a2fe09b531490969c9f4960086" + integrity sha512-vrgnLCKeu3orxmBhyjll8/rxnL3k2EYtIYa7D7GiJVYWmOE08J5S6c/1j15LnC3cQkYqYgOJR5JnX5gxooQGLA== punycode@^2.1.0: version "2.3.1" @@ -2740,7 +2735,7 @@ ts-jest@^29.0.4: semver "^7.5.3" yargs-parser "^21.0.1" -ts-node@^10.9.1: +ts-node@^10.9.2: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== From a881cda69192ca6556b96ac374b93664933ebca2 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Mon, 12 Feb 2024 12:33:49 +0100 Subject: [PATCH 54/54] nitpick: format --- source/optims/constantFolding.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/optims/constantFolding.ts b/source/optims/constantFolding.ts index 1e55f63..8e90243 100644 --- a/source/optims/constantFolding.ts +++ b/source/optims/constantFolding.ts @@ -267,10 +267,10 @@ function replaceRuleWithEvaluatedNodeValue( typeof nodeValue === 'number' ? 'number' : typeof nodeValue === 'boolean' - ? 'boolean' - : typeof nodeValue === 'string' - ? 'string' - : undefined, + ? 'boolean' + : typeof nodeValue === 'string' + ? 'string' + : undefined, nodeKind: 'constant', missingVariables: {},