-
Notifications
You must be signed in to change notification settings - Fork 115
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
(#426) Join binding followed by usage of binding leads to unresolvabl… #427
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -174,8 +174,10 @@ | |
;; that a rule wouldn't, or at least shouldn't for | ||
;; clarity, start the names of other locals or vars | ||
;; with "?". | ||
(mapv (comp symbol name) all-bindings))] | ||
(com/compile-action all-bindings | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at this again, i don't think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand this correctly all-bindings is being populated from the constraints after this transformation and that is why this change is needed? |
||
(mapv (comp symbol name) (:bindings beta-node)))] | ||
;; using the :bindings defined on the beta-node rather than `all-bindings`, as all-bindings | ||
;; does not account for bindings that reside within expression join nodes | ||
(com/compile-action (:bindings beta-node) | ||
;; Using private function for now as a workaround. | ||
(if (:ns-name production) | ||
(if (com/compiling-cljs?) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,6 @@ | |
[clojure.core.reducers :as r] | ||
[clojure.reflect :as reflect] | ||
[clojure.set :as s] | ||
[clojure.set :as set] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tried to clean up this binding |
||
[clojure.string :as string] | ||
[clojure.walk :as walk] | ||
[schema.core :as sc] | ||
|
@@ -803,17 +802,17 @@ | |
|
||
;; Unsatisfied conditions remain, so find ones we can satisfy. | ||
(let [satisfied? (fn [classified-condition] | ||
(clojure.set/subset? (:unbound classified-condition) | ||
bound-variables)) | ||
(s/subset? (:unbound classified-condition) | ||
bound-variables)) | ||
|
||
;; Find non-accumulator conditions that are satisfied. We defer | ||
;; accumulators until later in the rete network because they | ||
;; may fire a default value if all needed bindings earlier | ||
;; in the network are satisfied. | ||
satisfied-non-accum? (fn [classified-condition] | ||
(and (not (:is-accumulator classified-condition)) | ||
(clojure.set/subset? (:unbound classified-condition) | ||
bound-variables))) | ||
(s/subset? (:unbound classified-condition) | ||
bound-variables))) | ||
|
||
has-satisfied-non-accum (some satisfied-non-accum? remaining-conditions) | ||
|
||
|
@@ -825,16 +824,16 @@ | |
(remove satisfied-non-accum? remaining-conditions) | ||
(remove satisfied? remaining-conditions)) | ||
|
||
updated-bindings (apply clojure.set/union bound-variables | ||
updated-bindings (apply s/union bound-variables | ||
(map :bound newly-satisfied))] | ||
|
||
;; If no existing variables can be satisfied then the production is invalid. | ||
(when (empty? newly-satisfied) | ||
|
||
;; Get the subset of variables that cannot be satisfied. | ||
(let [unsatisfiable (clojure.set/difference | ||
(apply clojure.set/union (map :unbound still-unsatisfied)) | ||
bound-variables)] | ||
(let [unsatisfiable (s/difference | ||
(apply s/union (map :unbound still-unsatisfied)) | ||
bound-variables)] | ||
(throw (ex-info (str "Using variable that is not previously bound. This can happen " | ||
"when an expression uses a previously unbound variable, " | ||
"or if a variable is referenced in a nested part of a parent " | ||
|
@@ -855,11 +854,30 @@ | |
(defn- non-equality-unifications | ||
"Returns a set of unifications that do not use equality-based checks." | ||
[constraints] | ||
(let [[bound-variables unbound-variables] (classify-variables constraints)] | ||
(into #{} | ||
(for [constraint constraints | ||
:when (non-equality-unification? constraint bound-variables)] | ||
constraint)))) | ||
(let [[bound-variables _] (classify-variables constraints)] | ||
(loop [[cur & more] constraints | ||
constraints #{} | ||
bound-variables bound-variables] | ||
(if cur | ||
(let [non-equity? (non-equality-unification? cur bound-variables) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you meant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After exploring the The doc string is coming after arg list. |
||
|
||
;; In the event that the unification is also a binding, then we will need to remove | ||
;; the bound variable from the previously bound variables. This prevents further constraints | ||
;; that use this variable from being considered "equality-unifications". | ||
;; See https://github.com/cerner/clara-rules/issues/426 for more info. | ||
bound-variables (if (and non-equity? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I struggled with this a bit. I will summarize my understanding for clarity. The issue #426 description had an example with this form of [A (= ?a a)]
[B (= ?r (conj b ?a))
(not-empty? ?r)] Now consider the constraints for (= ?r (conj b ?a))
(not-empty? ?r) What is happening is that the I think it is important to note that this particular fn , Now Concretely, the next constraint is (non-equality-unification? '(not-empty? ?r) #{?r})
;;= false This would result in This approach works around this, by doing what Ethan appropriately called "re-classification". When a non-equality unification is detected, it has to remove it's newly introduced bindings from the classification as a mechanism to make other dependent constraints move along with it. I can follow this approach. However, it does seem like a sneaky way to utilize how bound variables are used to detect non-equality unifications. However, it seems that this may not work for some cases still: (defrecord A [a])
(defrecord B [b])
(defrule a-rule2
[A (= ?a a)]
[B (= ?r (conj b ?a))
(= ?r #{:a :b})]
=>
(println "doesn't matter")) ie. the 2nd constraint is an equality binding on This compiles, but it isn't producing a semantically correct network. The equality unification on (-> (mk-session [a-rule2] :cache false)
(insert (->A :c)
(->B #{:b}))
fire-rules)
;; Prints "doesn't matter"
I think the solution to this is in not trying to trying to manipulate only the classified There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding the a-rule2 case you describe above @mrrodriguez it looks to me like the problem is that the alpha node is creating the ?r binding, but the join filter is then rebinding it rather than testing for equality with the value created in the alpha network.
compile-constraints takes a collection of variables that it should not rebind, but in compile-join-filter this only includes variables from the ancestors. At first glance the solution would seem to be to pass along the element-bindings passed to compile-join-filter to compile-constraints. However, on closer examination, it doesn't appear to me that the element-bindings are necessarily correct here. They are populated from the :new-bindings of the beta network, which are just the bindings in the condition that aren't from the ancestors as can be seen in their population here. In order to fix this problem I think we need to store (correct) information on what bindings are created in the alpha network and which are created in the beta network, rather than just new-bindings information. Regarding this immediate PR, my current thinking is that there shouldn't be any need to start from the viewpoint of all bindings which are created in the condition; we can start from a "blank slate" of no bindings and proceed from there. We could then generate a list of bindings created in the alpha network and of bindings created in the beta network and use this to resolve this issue and the one described by @mrrodriguez above. Intuitively, the current solution seems a bit like we have incorrect binding analysis and are "cleaning it up" after the fact, while I'd suggest the cleaner solution would be to correct the binding analysis upfront. Note also that we do have the list of bindings from the ancestors available should it be needed, although at the moment I don't think we do. I also haven't tried implementing this concretely so it's possible that I'm missing something. This may also bring up issues of whether we want the constraints to be order-independent as well and figuring out what the current behaviour is. |
||
(seq cur) | ||
(equality-expression? cur)) | ||
(s/difference bound-variables | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps it'd be faster to use (->> cur
(filter is-variable?)
(apply disj bound-variables)) Edit: this approach may need to be changed and this particular code-change may no longer be relevant, see above. |
||
(into #{} | ||
(filter is-variable?) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is something of an edge case, but this can result in some constraints that are actually resolvable in the alpha network being considered non-equality-unifications since the variable could be previously bound. Example:
|
||
cur)) | ||
bound-variables) | ||
constraints (if non-equity? | ||
(conj constraints cur) | ||
constraints)] | ||
(recur more constraints bound-variables)) | ||
constraints)))) | ||
|
||
(sc/defn condition-to-node :- schema/ConditionNode | ||
"Converts a condition to a node structure." | ||
|
@@ -917,7 +935,7 @@ | |
constraint-bindings) | ||
|
||
new-bindings (s/difference (variables-as-keywords (:constraints condition)) | ||
parent-bindings) | ||
parent-bindings) | ||
|
||
join-filter-bindings (if join-filter-expressions | ||
(variables-as-keywords join-filter-expressions) | ||
|
@@ -1018,7 +1036,7 @@ | |
;; have the necessary bindings. | ||
;; See https://github.com/cerner/clara-rules/issues/304 for more details | ||
;; and a case that behaves incorrectly without this check. | ||
ancestor-bindings-in-negation-expr (set/intersection | ||
ancestor-bindings-in-negation-expr (s/intersection | ||
(variables-as-keywords negation-expr) | ||
ancestor-bindings) | ||
|
||
|
@@ -1224,8 +1242,8 @@ | |
;; for use in descendent nodes. | ||
{:beta-graph (:beta-graph new-result) | ||
:new-ids (into (:new-ids previous-result) (:new-ids new-result)) | ||
:bindings (set/union (:bindings previous-result) | ||
(:bindings new-result))})) | ||
:bindings (s/union (:bindings previous-result) | ||
(:bindings new-result))})) | ||
|
||
;; Initial reduce value, combining previous graph, parent ids, and ancestor variable bindings. | ||
{:beta-graph beta-with-negations | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,14 +12,15 @@ | |
query]] | ||
|
||
[clara.rules.testfacts :refer [->Temperature ->Cold ->WindSpeed | ||
->ColdAndWindy]] | ||
->ColdAndWindy ->WindChill]] | ||
[clojure.test :refer [is deftest run-tests testing use-fixtures]] | ||
[clara.rules.accumulators :as acc] | ||
[schema.test :as st]) | ||
(:import [clara.rules.testfacts | ||
Temperature | ||
Cold | ||
WindSpeed | ||
WindChill | ||
ColdAndWindy])) | ||
|
||
:cljs | ||
|
@@ -33,6 +34,7 @@ | |
[clara.rules.testfacts :refer [->Temperature Temperature | ||
->Cold Cold | ||
->WindSpeed WindSpeed | ||
->WindChill WindChill | ||
->ColdAndWindy ColdAndWindy]] | ||
[clara.rules.accumulators :as acc] | ||
[cljs.test] | ||
|
@@ -369,3 +371,70 @@ | |
fire-rules | ||
(query cold-query))) | ||
"The query results should not be empty for a matching location")) | ||
|
||
;; https://github.com/cerner/clara-rules/issues/426 | ||
;; A join binding used in the same condition but different constraint will fail to compile. | ||
(defn calc-wind-chill | ||
[temp wind-speed] | ||
(+ 35.74 | ||
(- (* 0.6215 temp) | ||
(* 35.75 | ||
(Math/pow wind-speed 0.16))) | ||
(* 0.4275 temp (Math/pow wind-speed 0.16)))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing this is a formula for wind chill? It seems unnecessarily complicated to use a real formula here since what we're testing has nothing to with this. Also does "Math/pow" work in ClojureScript? |
||
|
||
(def-rules-test test-join-binding-followed-by-usage | ||
{:rules [cold-windy-rule [[[Temperature | ||
(= ?t temperature) | ||
(= ?loc location)] | ||
[WindSpeed | ||
(= location ?loc) | ||
(= ?wc (calc-wind-chill ?t windspeed)) | ||
(<= ?wc 32)]] | ||
(insert! (->WindChill ?t ?loc ?wc))]] | ||
:queries [wind-chill-query [[] | ||
[[WindChill | ||
(= ?t temperature) | ||
(= ?wind-chill wind-chill) | ||
(= ?loc location)]]]] | ||
:sessions [empty-session [cold-windy-rule wind-chill-query] {}]} | ||
;; The overall intent of this test is to prove that the rules above compile and | ||
;; operate as expected. | ||
(is (= [{:?loc "LHR" | ||
:?t 36 | ||
:?wind-chill (calc-wind-chill 36 7)}] | ||
(-> empty-session | ||
(insert (->Temperature 36 "LHR")) | ||
(insert (->WindSpeed 7 "LHR")) | ||
fire-rules | ||
(query wind-chill-query))) | ||
;; Not the intent of the test, but a distinct message in the event the test fails. | ||
"The query should return the wind-chill for the given location since the wind chill is under 32 degrees")) | ||
|
||
;; https://github.com/cerner/clara-rules/issues/426 | ||
;; Due to the way we compile the beta-network for cljs this test would fail for cljs because the binding | ||
;; expression join node(?wc) would not be recognised correctly in the production node. Thus ?wc would be | ||
;; assumed to be a variable defined in this ns and would resolve to nil at runtime. | ||
(def-rules-test test-join-binding-used-in-a-produciton-node | ||
{:rules [cold-windy-rule [[[Temperature | ||
(= ?t temperature) | ||
(= ?loc location)] | ||
[WindSpeed | ||
(= location ?loc) | ||
(= ?wc (calc-wind-chill ?t windspeed))]] | ||
(insert! (->WindChill ?t ?loc ?wc))]] | ||
:queries [wind-chill-query [[] | ||
[[WindChill | ||
(= ?t temperature) | ||
(= ?wind-chill wind-chill) | ||
(= ?loc location)]]]] | ||
:sessions [empty-session [cold-windy-rule wind-chill-query] {}]} | ||
(is (= [{:?loc "LHR" | ||
:?t 36 | ||
:?wind-chill (calc-wind-chill 36 7)}] | ||
(-> empty-session | ||
(insert (->Temperature 36 "LHR")) | ||
(insert (->WindSpeed 7 "LHR")) | ||
fire-rules | ||
(query wind-chill-query))) | ||
;; Not the intent of the test, but a distinct message in the event the test fails. | ||
"The query should return the wind-chill for the given location since there is a Temperature and WindSpeed")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "surrrouning"