/
macros.clj
277 lines (239 loc) · 11.8 KB
/
macros.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
(ns clara.macros
"Direct use of this namespace is deprecated. Users can now
simply use the defrule, defquery, and defsession macros
in the clara.rules namespace. Users can simply pull those macros
in like any other, for instance:
(:require [clara.rules :refer [insert fire-rules query insert!]
:refer-macros [defrule defsession defquery]])
"
(:require [clara.rules.engine :as eng]
[clara.rules.memory :as mem]
[clara.rules.compiler :as com]
[clara.rules.dsl :as dsl]
[cljs.analyzer :as ana]
[cljs.env :as env]
[schema.core :as sc]
[clara.rules.schema :as schema]
[clojure.set :as s]))
;;; Clear productions stored in cljs.env/*compiler* for current namespace.
;;; Only exists for its side-effect, hence returns nil.
(defmacro clear-ns-productions!
[]
(swap! env/*compiler* assoc-in [::productions (com/cljs-ns)] {})
nil)
;; Store production in cljs.env/*compiler* under ::productions seq?
(defn- add-production [name production]
(swap! env/*compiler* assoc-in [::productions (com/cljs-ns) name] production))
(defn- get-productions-from-namespace
"Returns a map of names to productions in the given namespace."
[namespace]
;; TODO: remove need for ugly eval by changing our quoting strategy.
(let [productions (get-in @env/*compiler* [::productions namespace])]
(map eval (vals productions))))
(defn- get-productions
"Return the productions from the source"
[source]
(cond
(symbol? source) (get-productions-from-namespace source)
(coll? source) (seq source)
:else (throw (IllegalArgumentException. "Unknown source value type passed to defsession"))))
(defn defrule!
[name production]
(add-production name production)
`(def ~name
~production))
(defmacro defrule
[name & body]
(defrule! name (dsl/build-rule name body)))
(defn defquery!
[name query]
(add-production name query)
`(def ~name
~query))
(defmacro defquery
[name & body]
(defquery! name (dsl/build-query name body)))
(sc/defn gen-beta-network :- [sc/Any] ; Returns a sequence of compiled nodes.
"Generates the beta network from the beta tree. "
([node-ids :- #{sc/Int} ; Nodes to compile.
{:keys [id-to-production-node id-to-condition-node id-to-new-bindings forward-edges] :as beta-graph} :- schema/BetaGraph
parent-bindings :- #{sc/Keyword}]
(vec
(for [id node-ids
:let [beta-node (or (get id-to-condition-node id)
(get id-to-production-node id))
{:keys [condition production query join-bindings]} beta-node
child-ids (get forward-edges id)
constraint-bindings (com/variables-as-keywords (:constraints condition))
;; Get all bindings from the parent, condition, and returned fact.
all-bindings (cond-> (s/union parent-bindings constraint-bindings)
;; Optional fact binding from a condition.
(:fact-binding condition) (conj (:fact-binding condition))
;; Optional accumulator result.
(:result-binding beta-node) (conj (:result-binding beta-node)))
new-bindings (get id-to-new-bindings id)]]
(case (:node-type beta-node)
:join
(if (:join-filter-expressions beta-node)
`(eng/->ExpressionJoinNode
~id
'~condition
~(com/compile-join-filter id
"ExpressionJoinNode"
(:join-filter-expressions beta-node)
(:join-filter-join-bindings beta-node)
(:new-bindings beta-node)
{})
~(gen-beta-network child-ids beta-graph all-bindings)
~join-bindings)
`(eng/->HashJoinNode
~id
'~condition
~(gen-beta-network child-ids beta-graph all-bindings)
~join-bindings))
:negation
(if (:join-filter-expressions beta-node)
`(eng/->NegationWithJoinFilterNode
~id
'~condition
~(com/compile-join-filter id
"NegationWithJoinFilterNode"
(:join-filter-expressions beta-node)
(:join-filter-join-bindings beta-node)
(:new-bindings beta-node)
{})
~(gen-beta-network child-ids beta-graph all-bindings)
~join-bindings)
`(eng/->NegationNode
~id
'~condition
~(gen-beta-network child-ids beta-graph all-bindings)
~join-bindings))
:test
`(eng/->TestNode
~id
~(com/compile-test id (:constraints condition))
~(gen-beta-network child-ids beta-graph all-bindings))
:accumulator
(if (:join-filter-expressions beta-node)
`(eng/->AccumulateWithJoinFilterNode
~id
{:accumulator '~(:accumulator beta-node)
:from '~condition}
~(:accumulator beta-node)
~(com/compile-join-filter id
"AccumulateWithJoinFilterNode"
(:join-filter-expressions beta-node)
(:join-filter-join-bindings beta-node)
(:new-bindings beta-node)
{})
~(:result-binding beta-node)
~(gen-beta-network child-ids beta-graph all-bindings)
~join-bindings
~new-bindings)
`(eng/->AccumulateNode
~id
{:accumulator '~(:accumulator beta-node)
:from '~condition}
~(:accumulator beta-node)
~(:result-binding beta-node)
~(gen-beta-network child-ids beta-graph all-bindings)
~join-bindings
~new-bindings))
:production
`(eng/->ProductionNode
~id
'~production
;; NOTE: This is a workaround around allowing the compiler to eval this
;; form in an unknown ns. It will suffer from var shadowing problems described
;; @ https://github.com/cerner/clara-rules/issues/178.
;; A better solution may be to enhance dsl/resolve-vars to deal with shadowing
;; correctly in cljs. This may be easier to do there since the compiler's
;; analyzer may be more exposed than on the clj side.
~(let [resolve-vars #(@#'dsl/resolve-vars (:rhs production)
;; It is unlikely that passing the bindings matters,
;; but all:fact-binding from the LHS conditions were
;; made available here in the past. However, all
;; bindings begin with "?" which typically means
;; 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 id
all-bindings
;; Using private function for now as a workaround.
(if (:ns-name production)
(if (com/compiling-cljs?)
(binding [cljs.analyzer/*cljs-ns* (:ns-name production)]
(resolve-vars))
(binding [*ns* (the-ns (:ns-name production))]
(resolve-vars)))
(resolve-vars))
(:env production))))
:query
`(eng/->QueryNode
~id
'~query
~(:params query))
(throw (ex-info (str "Unknown node type " (:node-type beta-node)) {:node beta-node})))))))
(sc/defn ^:always-validate compile-alpha-nodes
[alpha-nodes :- [schema/AlphaNode]]
(vec
(for [{:keys [id condition beta-children env]} alpha-nodes
:let [{:keys [type constraints fact-binding args]} condition]]
{:id id
:type (com/effective-type type)
:alpha-fn (com/compile-condition type id (first args) constraints fact-binding env)
:children (vec beta-children)})))
(defn productions->session-assembly-form
[productions options]
;;; Calling com/names-unique here rather than in sources-and-options->session-assembly-form
;;; as a ClojureScript DSL may call productions->session-assembly-form if that DSL
;;; has its own mechanism for grouping rules which is different than the clara DSL.
(com/validate-names-unique productions)
(let [id-counter (atom 0)
create-id-fn (fn [] (swap! id-counter inc))
beta-graph (com/to-beta-graph productions create-id-fn)
;; Compile the children of the logical root condition.
beta-network (gen-beta-network (get-in beta-graph [:forward-edges 0]) beta-graph #{})
alpha-graph (com/to-alpha-graph beta-graph create-id-fn)
alpha-nodes (compile-alpha-nodes alpha-graph)]
`(let [beta-network# ~beta-network
alpha-nodes# ~alpha-nodes
productions# '~productions
options# ~options]
(clara.rules/assemble-session beta-network# alpha-nodes# productions# options#))))
(defn sources-and-options->session-assembly-form
[sources-and-options]
(let [sources (take-while #(not (keyword? %)) sources-and-options)
options (apply hash-map (drop-while #(not (keyword? %)) sources-and-options))
;; Eval to unquote ns symbols, and to eval exprs to look up
;; explicit rule sources
sources (eval (vec sources))
productions (vec (for [source sources
production (get-productions source)]
production))]
(productions->session-assembly-form productions options)))
(defmacro defsession
"Creates a session given a list of sources and keyword-style options, which are typically ClojureScript namespaces.
Each source is eval'ed at compile time, in Clojure (not ClojureScript.)
If the eval result is a symbol, it is presumed to be a ClojureScript
namespace, and all rules and queries defined in that namespace will
be found and used.
If the eval result is a collection, it is presumed to be a
collection of productions. Note that although the collection must
exist in the compiling Clojure runtime (since the eval happens at
macro-expansion time), any expressions in the rule or query
definitions will be executed in ClojureScript.
Typical usage would be like this, with a session defined as a var:
(defsession my-session 'example.namespace)
That var contains an immutable session that then can be used as a starting point to create sessions with
caller-provided data. Since the session itself is immutable, it can be safely used from multiple threads
and will not be modified by callers. So a user might grab it, insert facts, and otherwise
use it as follows:
(-> my-session
(insert (->Temperature 23))
(fire-rules))
"
[name & sources-and-options]
`(def ~name ~(sources-and-options->session-assembly-form sources-and-options)))