I am trying to write a domain-specific production rule language which is compiled down at run-time into Clojure code. It's intended that the rule language should be usable by primary school children with little help.
So far it's going extremely well, except for one problem. If I run it in the REPL, like this, it works:
user=> (use 'mw-parser.core :reload)
user=> (use 'mw-engine.utils)
user=> (parse-rule "if state is forest and fertility is between 55 and 75 then state should be climax")
(fn [cell world] (if (and (= (:state cell) :forest) (or (< 55 (get-int cell :fertility) 75) (> 55 (get-int cell :fertility) 75))) (merge cell {:state :climax})))
#<user$eval1839$fn__1840 user$eval1839$fn__1840@7a52b16b>
user=> (apply *1 (list {:state :forest :fertility 60} nil))
{:state :climax, :fertility 60}
However, I have a test as follows:
(:require [clojure.test :refer :all]
[mw-parser.core :refer :all]))
(testing "if altitude is less than 100 and state is forest then state should be climax and deer should be 3"
(is (parse-rule "if altitude is less than 100 and state is forest then state should be climax and deer should be 3"))
(is (let [cell (apply (eval (parse-rule "if altitude is less than 100 and state is forest then state should be climax and deer should be 3"))
(list {:state :forest :altitude 99} nil))]
(and (= (:state cell) :climax) (= (:deer cell) 3))))
This fails as follows:
simon@engraver:~/workspace/mw-parser$ lein test
lein test mw-parser.core-test
lein test :only mw-parser.core-test/rules-tests
ERROR in (rules-tests) (Compiler.java:6380)
if altitude is less than 100 and state is forest then state should be climax and deer should be 3
expected: (let [cell (apply (eval (parse-rule "if altitude is less than 100 and state is forest then state should be climax and deer should be 3")) (list {:state :forest, :altitude 99} nil))] (and (= (:state cell) :climax) (= (:deer cell) 3)))
actual: clojure.lang.Compiler$CompilerException: java.lang.RuntimeException: Unable to resolve symbol: get-int in this context, compiling:(/tmp/form-init4592216274934008360.clj:1:6384)
The function get-int is in mw-engine.utils, and is:
(defn get-int
"Get the value of a property expected to be an integer from a map; if not present (or not an integer) return 0.
* `map` a map;
* `key` a symbol or keyword, presumed to be a key into the `map`."
[map key]
(cond map
(let [v (map key)]
(cond (and v (integer? v)) v
true 0))
true (throw (Exception. "No map passed?"))))
I'm trying to understand why this function is not available in the test environment when the anonymous function generated from the rule text is compiled and applied. This matters, because I expect the users of the system to add rules via a web form, so they won't have a REPL.
As you can see I've specified that mw-engine.utils is used by the test file, but that does not apparently make the namespace available in eval. It is the eval step, not the apply step, that fails, I've verified that by trying:
user=> (use 'mw-parser.core :reload)
user=> (parse-rule "if state is forest and fertility is between 55 and 75 then state should be climax")
(fn [cell world] (if (and (= (:state cell) :forest) (or (< 55 (get-int cell :fertility) 75) (> 55 (get-int cell :fertility) 75))) (merge cell {:state :climax})))
CompilerException java.lang.RuntimeException: Unable to resolve symbol: get-int in this context, compiling:(/tmp/form-init2345285067501587397.clj:1:1)
(Note that on this occasion I didn't include the (use 'mw-engine.utils) step)
Any assistance gratefully received!