Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expression comparison for node sharing does not take into account parameter positions #246

Closed
larrybehaviorlanguage opened this issue Dec 27, 2020 · 1 comment
Assignees
Labels
Milestone

Comments

@larrybehaviorlanguage
Copy link

The code below was tested with the latest version of the NRules develop branch (b1d9074 of December 26, 2020 at 3:29:35 PM PST).

Looking at ThreeFactOrBetaTestRule, this specific ordering and reference to the matched myNameFact and otherNameFact for some reason causes this rule not to fire.

using System;
using System.Linq;
using NRules.Fluent.Dsl;
using NRules.IntegrationTests.TestAssets;
using NRules.RuleModel;
using Xunit;

namespace NRules.IntegrationTests
{
    public class ThreeFactOrBetaTest : BaseRuleTestFixture
    {

        [Fact]
        public void ThreeFactOrBeta()
        {
            //Arrange
            var myNameFact = new NameFact { Name = "myName" };
            var otherNameFact = new NameFact { Name = "otherName" };
            var referenceFact = new ReferenceFact { ReferenceA = myNameFact, ReferenceB = otherNameFact };

            Session.Insert(myNameFact);
            Session.Insert(otherNameFact);
            Session.Insert(referenceFact);

            //Act
            Session.Fire();

            //Assert
            AssertFiredOnce();
        }


        protected override void SetUpRules()
        {
            SetUpRule<ThreeFactOrBetaTestRule>();
        }
        
        public class NameFact
        {
            public string Name { get; set; }
        }

        public class ReferenceFact
        {
            public NameFact ReferenceA { get; set; }
            public NameFact ReferenceB { get; set; }
        }   

        public class ThreeFactOrBetaTestRule : Rule
        {
            public Action<IContext> Action = ctx => { };

            public override void Define()
            {
                NameFact myNameFact = null;
                NameFact otherNameFact = null;
                ReferenceFact refFact = null;

            When()
                .Match<NameFact>(() => myNameFact, f => f.Name == "myName")
                .Match<NameFact>(() => otherNameFact, f => f.Name == "otherName")

                .Or(x => x
                    .Match<ReferenceFact>(() => refFact, rf => rf.ReferenceA == myNameFact, rf => rf.ReferenceB == myNameFact)
                    .Match<ReferenceFact>(() => refFact, rf => rf.ReferenceA == myNameFact, rf => rf.ReferenceB == otherNameFact)
                );
                ;

                Then()
                    .Do(ctx => Action(ctx));
            }
        }
    }
}

In testing, the following all alter or correct the issue:

  • Reversing the order of the two clauses in the or group causes the rule fire, but it incorrectly fires twice
  • Changing the last test in the first or clause from 'rf => rf.ReferenceB == myNameFact' to 'rf => rf.ReferenceB == null' causes the rule to run properly
  • Adding a call to a boolean-returning method at the end of the second clause's final test causes the rule to properly fire once, as shown below:
public class ThreeFactOrBetaTestRule : Rule
        {
            public Action<IContext> Action = ctx => { };

            public bool ReturnsTrue() { return true; }

            public override void Define()
            {
                NameFact myNameFact = null;
                NameFact otherNameFact = null;
                ReferenceFact refFact = null;

            When()
                .Match<NameFact>(() => myNameFact, f => f.Name == "myName")
                .Match<NameFact>(() => otherNameFact, f => f.Name == "otherName")

                .Or(x => x
                    .Match<ReferenceFact>(() => refFact, rf => rf.ReferenceA == myNameFact, rf => rf.ReferenceB == null)
                    .Match<ReferenceFact>(() => refFact, rf => rf.ReferenceA == myNameFact, rf => rf.ReferenceB == otherNameFact && ReturnsTrue())
                );
                ;

                Then()
                    .Do(ctx => Action(ctx));
            }
        }
@snikolayev snikolayev self-assigned this Dec 27, 2020
@snikolayev snikolayev added this to the 0.9.2 milestone Dec 31, 2020
@snikolayev snikolayev changed the title Rule doesn't fire with specific order of beta Or group clauses Expression comparison for node sharing does not take into account parameter positions Jan 3, 2021
@snikolayev
Copy link
Member

A rule with an OR clause is actually transformed into two rules. So, this issue is not just when using an OR clause, but can happen with any set of rules that share conditions and participate in expression comparison for node sharing.
The below two rules expose the issue:

    public class NodeSharingNonEquivalentTest : BaseRuleTestFixture
    {
        [Fact]
        public void Fire_FirstRuleMatchesFacts_OnlyFirstRuleFires()
        {
            //Arrange
            var myNameFact = new NameFact { Name = "myName" };
            var otherNameFact = new NameFact { Name = "otherName" };
            var referenceFact = new ReferenceFact { Reference = myNameFact };

            Session.Insert(myNameFact);
            Session.Insert(otherNameFact);
            Session.Insert(referenceFact);

            //Act
            Session.Fire();

            //Assert
            AssertFiredOnce<TestRule1>();
            AssertDidNotFire<TestRule2>();
        }
        
        [Fact]
        public void Fire_SecondRuleMatchesFacts_OnlySecondRuleFires()
        {
            //Arrange
            var myNameFact = new NameFact { Name = "myName" };
            var otherNameFact = new NameFact { Name = "otherName" };
            var referenceFact = new ReferenceFact { Reference = otherNameFact };

            Session.Insert(myNameFact);
            Session.Insert(otherNameFact);
            Session.Insert(referenceFact);

            //Act
            Session.Fire();

            //Assert
            AssertDidNotFire<TestRule1>();
            AssertFiredOnce<TestRule2>();
        }

        protected override void SetUpRules()
        {
            SetUpRule<TestRule1>();
            SetUpRule<TestRule2>();
        }
        
        public class NameFact
        {
            public string Name { get; set; }
        }

        public class ReferenceFact
        {
            public NameFact Reference { get; set; }
        }   

        public class TestRule1 : Rule
        {
            public override void Define()
            {
                NameFact myNameFact1 = null;
                NameFact otherNameFact1 = null;
                ReferenceFact refFact1 = null;

                When()
                    .Match(() => myNameFact1, f => f.Name == "myName")
                    .Match(() => otherNameFact1, f => f.Name == "otherName")
                    .Match(() => refFact1, rf => rf.Reference == myNameFact1);

                Then()
                    .Do(ctx => ctx.NoOp());
            }
        }

        public class TestRule2 : Rule
        {
            public override void Define()
            {
                NameFact myNameFact2 = null;
                NameFact otherNameFact2 = null;
                ReferenceFact refFact2 = null;

                When()
                    .Match(() => myNameFact2, f => f.Name == "myName")
                    .Match(() => otherNameFact2, f => f.Name == "otherName")
                    .Match(() => refFact2, rf => rf.Reference == otherNameFact2);

                Then()
                    .Do(ctx => ctx.NoOp());
            }
        }
    }

This results in this rete graph where all nodes for these two rules got fully shared, even though the reference expressions are referencing different instances of the NameFact.
NodeSharing-Rete-Bad
This is what the resulting graph should be:
NodeSharing-Rete-Good
In this case the first two matches are fully shared, but only the alpha network is shared for the third match, and beta sub-graph is not shared as join expressions are different.

The root issue here is that expression comparison does not account for parameter positions in the match tuple, when deciding which nodes to share. In this case, given there are multiple instances of the NameFact matched by the rule, the comparison fails to distinguish between these different instances.

@snikolayev snikolayev added the bug label Jan 3, 2021
larrybehaviorlanguage added a commit to larrybehaviorlanguage/NRules that referenced this issue Jan 4, 2021
* bl-develop: (39 commits)
  Update year
  Update year
  Expression comparison to check parameter positions. Fixes NRules#246
  Code cleanup
  Improve quality of integration test assertions
  Use https in project URL
  Increment version
  Upgrade Sandcastle and fix docs build
  Add missing xml doc
  Revert "Remove msbuild from build tooling"
  Cleanup the sample
  Code cleanup
  Cleanup rules compilation method signatures
  Minor code cleanup
  Upgrade Moq and xunit
  Code cleanup - activation methods naming
  Convert default parameters to method overloads
  Fix Autofac integration. Closes NRules#238
  Cleanup samples
  Expose IMatch on the agenda-related events
  ...

# Conflicts:
#	src/NRules/NRules/Rete/JoinNode.cs. Caused by BoolObj addition to JoinNode.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants