diff --git a/.gitignore b/.gitignore index e050faa..b0f5637 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ pom.xml .clj-kondo/ .lsp/ .nrepl-port + +doc/scratch.clj diff --git a/src/mw_parser/declarative.clj b/src/mw_parser/declarative.clj index c2790a4..910c5e5 100644 --- a/src/mw_parser/declarative.clj +++ b/src/mw_parser/declarative.clj @@ -157,16 +157,18 @@ Throws an exception if parsing fails." ([rule-text return-tuple?] - (let [lines (remove comment? (split-lines rule-text))] + (let [lines (map trim (remove comment? (split-lines rule-text)))] (if (> (count lines) 1) (map #(compile % return-tuple?) lines) - (let [src (trim rule-text) - parse-tree (simplify (parse src)) - fn' (generate parse-tree) + (let [src (first lines) + parse-tree (doall (simplify (parse src))) + fn' (doall (generate parse-tree)) afn (try - (if (= 'fn (first fn')) + (if (#{'fn 'fn*} (first fn')) (vary-meta (eval fn') merge (meta fn')) - (throw (Exception. (format "Parse of `%s` did not return a functionn" src)))) + (throw (Exception. + (format "Parse of `%s` did not return a function: %s" + src fn')))) (catch Exception any (throw (ex-info (.getMessage any) {:src src :parse parse-tree diff --git a/src/mw_parser/generate.clj b/src/mw_parser/generate.clj index af8649b..c7da5cc 100644 --- a/src/mw_parser/generate.clj +++ b/src/mw_parser/generate.clj @@ -25,13 +25,20 @@ (declare generate generate-action) +;;; macros used in generated rules ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;; production (if-then) rules ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn generate-rule "From this `tree`, assumed to be a syntactically correct rule specification, generate and return the appropriate rule as a function of two arguments." [tree] (assert-type tree :RULE) - (vary-meta - (list 'fn ['cell 'world] (list 'when (generate (nth tree 2)) (generate (nth tree 3)))) + (vary-meta + ;; do macro-expansion here, because at least in theory I know what + ;; macros are in scope here. + (macroexpand + (list 'fn ['cell 'world] (list 'when (generate (nth tree 2)) (generate (nth tree 3))))) merge {:rule-type :production})) @@ -275,28 +282,36 @@ {:tree tree} x))))) -;;; Flow rules. A flow rule DOES NOT return a modified world; instead, it -;;; returns a PLAN to modify the world, in the form of a sequence of `flows`. -;;; It is only when the plan is executed that the world is modified. +;;; Flow rules. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; A flow rule DOES NOT return a modified cell; instead, it +;;; returns a PLAN to modify the world, in the form of a sequence of `flow` +;;; objects. See `mw-engine.flow` ;;; -;;; so we're looking at something like -;;; (fn [cell world]) -;;; (if (= (:state cell) (or (:house cell) :house)) +;;; It is only when the plan is executed that the world is modified. (defn flow-rule "Generate a flow rule for this `quantity` of this `property` from this - `source` to this `destination`." + `source` to this `destination`. + + A flow rule **does not** return a modified cell; instead, it + returns a **plan** to modify the world, in the form of a sequence of + `flow` objects. See `mw-engine.flow` + + It is only when the plan is executed that the world is modified." [source property quantity-frag destinations] (vary-meta - (list 'fn ['cell 'world] - (list 'when (list 'and source (list 'pos? (list 'cell property))) - (list 'map - (list 'fn ['d] - {:source (list 'select-keys 'cell [:x :y]) - :destination (list 'select-keys 'd [:x :y]) - :property property - :quantity quantity-frag}) - destinations))) + ;; do macro-expansion here, because at least in theory I know what + ;; macros are in scope here. + (macroexpand + (list 'fn ['cell 'world] + (list 'when (list 'and source (list 'pos? (list 'cell property))) + (list 'map + (list 'fn ['d] + {:source (list 'select-keys 'cell [:x :y]) + :destination (list 'select-keys 'd [:x :y]) + :property property + :quantity quantity-frag}) + destinations)))) merge {:rule-type :flow})) diff --git a/test/mw_parser/declarative_test.clj b/test/mw_parser/declarative_test.clj index 7b01eec..8d3f8b4 100644 --- a/test/mw_parser/declarative_test.clj +++ b/test/mw_parser/declarative_test.clj @@ -3,7 +3,7 @@ [mw-engine.core :refer [transform-world]] [mw-engine.utils :refer [get-cell]] [mw-engine.world :refer [make-world]] - [mw-parser.declarative :refer [compile parse-rule]] + [mw-parser.declarative :refer [compile parse parse-rule]] [mw-parser.utils :refer [rule?]])) (deftest rules-tests @@ -34,21 +34,20 @@ (deftest exception-tests (testing "Constructions which should cause exceptions to be thrown" (is (thrown-with-msg? Exception #"^I did not understand.*" - (compile "the quick brown fox jumped over the lazy dog")) + (parse "the quick brown fox jumped over the lazy dog")) "Exception thrown if rule text does not match grammar") (is (thrown-with-msg? Exception #"^I did not understand.*" - (compile "if i have a cat on my lap then everything is fine")) + (parse "if i have a cat on my lap then everything is fine")) "Exception thrown if rule text does not match grammar") (is (thrown-with-msg? Exception #"The properties 'x' and 'y' of a cell are reserved and should not be set in rule actions" - (compile "if state is new then x should be 0")) + (parse "if state is new then x should be 0")) "Exception thrown on attempt to set 'x'") (is (thrown-with-msg? Exception #"The properties 'x' and 'y' of a cell are reserved and should not be set in rule actions" - (compile "if state is new then y should be 0")) + (parse "if state is new then y should be 0")) "Exception thrown on attempt to set 'y'"))) - (deftest correctness-tests ;; these are, in so far as possible, the same as the correctness-tests in core-tests - i.e., the two compilers ;; compile the same language. diff --git a/test/mw_parser/flow_test.clj b/test/mw_parser/flow_test.clj index 71b0c63..77dbaea 100644 --- a/test/mw_parser/flow_test.clj +++ b/test/mw_parser/flow_test.clj @@ -1,69 +1,43 @@ (ns mw-parser.flow-test (:require ;; [clojure.pprint :as pprint] - [clojure.test :refer [deftest is testing]] ;; [mw-engine.core :refer [transform-world]] - [mw-parser.declarative :refer [parse]] + [clojure.test :refer [deftest is testing]] ;; [mw-engine.core :refer [transform-world]] + [mw-parser.declarative :refer [parse]] [mw-parser.simplify :refer [simplify]])) (deftest parse-flow-tests (testing "flow-grammar" (let [rule "flow 1 food from house having food more than 10 to house within 2 with least food" + expected '(:FLOW-RULE + (:FLOW "flow") + (:QUANTITY (:SIMPLE-EXPRESSION (:NUMBER "1"))) + (:SYMBOL "food") + (:FROM "from") + (:SOURCE + (:PROPERTY-CONDITION (:SYMBOL "state") (:QUALIFIER (:EQUIVALENCE (:IS "is"))) (:EXPRESSION (:VALUE [:SYMBOL "house"]))) + (:WITH "having") + (:PROPERTY-CONDITION (:SYMBOL "food") (:QUALIFIER (:COMPARATIVE-QUALIFIER (:MORE "more") (:THAN "than"))) (:NUMBER "10"))) + (:TO-HOW (:TO "to")) + (:DESTINATION + (:TARGET + (:PROPERTY-CONDITION (:SYMBOL "state") (:QUALIFIER (:EQUIVALENCE (:IS "is"))) (:EXPRESSION (:VALUE [:SYMBOL "house"]))) + (:RANGE (:WITHIN "within") (:NUMBER "2"))) + (:WITH "with") + (:DETERMINER-CONDITION (:DETERMINER (:LEAST "least")) (:SYMBOL "food")))) + + actual (simplify (parse rule))] + + (is (= actual expected) rule)) + (let [rule "flow 10% food from house having food more than 10 to each house within 2 with food less than 4" expected '(:FLOW-RULE - (:SIMPLE-EXPRESSION (:NUMBER "1")) + (:FLOW "flow") + (:QUANTITY (:PERCENTAGE (:NUMBER "10") "%")) (:SYMBOL "food") (:FROM "from") - (:SOURCE - (:PROPERTY-CONDITION - (:SYMBOL "state") - (:QUALIFIER (:EQUIVALENCE (:IS "is"))) - (:EXPRESSION (:VALUE [:SYMBOL "house"]))) - (:WITH "having") - (:PROPERTY-CONDITION - (:SYMBOL "food") - (:QUALIFIER (:COMPARATIVE-QUALIFIER (:MORE "more") (:THAN "than"))) - (:NUMBER "10"))) - (:TO-HOW (:TO "to")) + (:SOURCE (:PROPERTY-CONDITION (:SYMBOL "state") (:QUALIFIER (:EQUIVALENCE (:IS "is"))) (:EXPRESSION (:VALUE [:SYMBOL "house"]))) (:WITH "having") (:PROPERTY-CONDITION (:SYMBOL "food") (:QUALIFIER (:COMPARATIVE-QUALIFIER (:MORE "more") (:THAN "than"))) (:NUMBER "10"))) + (:TO-HOW (:TO-EACH (:TO "to") (:EACH "each"))) (:DESTINATION - (:PROPERTY-CONDITION - (:SYMBOL "state") - (:QUALIFIER (:EQUIVALENCE (:IS "is"))) - (:EXPRESSION (:VALUE [:SYMBOL "house"]))) - (:WITHIN "within") - (:VALUE (:NUMBER "2")) - (:WITH "with") - (:FLOW-CONDITIONS - (:DETERMINER-CONDITION (:LEAST "least") (:SYMBOL "food"))))) + (:TARGET (:PROPERTY-CONDITION (:SYMBOL "state") (:QUALIFIER (:EQUIVALENCE (:IS "is"))) (:EXPRESSION (:VALUE [:SYMBOL "house"]))) + (:RANGE (:WITHIN "within") (:NUMBER "2"))) + (:WITH "with") (:PROPERTY-CONDITION (:SYMBOL "food") (:QUALIFIER (:COMPARATIVE-QUALIFIER (:LESS "less") (:THAN "than"))) (:NUMBER "4")))) actual (simplify (parse rule))] - - (is (= actual expected) rule)) - (let [rule "flow 10% food from house having food more than 10 to each house within 2 with food less than 4" - expected '(:FLOW-RULE - (:FLOW "flow") - (:QUANTITY (:PERCENTAGE (:NUMBER "10") "%")) - (:SYMBOL "food") - (:FROM "from") - (:SOURCE - (:PROPERTY-CONDITION - (:SYMBOL "state") - (:QUALIFIER (:EQUIVALENCE (:IS "is"))) - (:EXPRESSION (:VALUE [:SYMBOL "house"]))) - (:WITH "having") - (:PROPERTY-CONDITION - (:SYMBOL "food") - (:QUALIFIER - (:COMPARATIVE-QUALIFIER (:MORE "more") (:THAN "than"))) - (:NUMBER "10"))) - (:TO-HOW (:TO-EACH (:TO "to") (:EACH "each"))) - (:DESTINATION - (:TARGET - (:PROPERTY-CONDITION - (:SYMBOL "state") (:QUALIFIER (:EQUIVALENCE (:IS "is"))) - (:EXPRESSION (:VALUE [:SYMBOL "house"]))) - (:RANGE (:WITHIN "within") (:NUMBER "2"))) - (:WITH "with") - (:PROPERTY-CONDITION - (:SYMBOL "food") - (:QUALIFIER - (:COMPARATIVE-QUALIFIER (:LESS "less") (:THAN "than"))) - (:NUMBER "4")))) - actual (simplify (parse rule))] - (is (= actual expected) rule)))) + (is (= actual expected) rule)))) diff --git a/test/mw_parser/generate_test.clj b/test/mw_parser/generate_test.clj index 52566dd..a097c6d 100644 --- a/test/mw_parser/generate_test.clj +++ b/test/mw_parser/generate_test.clj @@ -77,10 +77,10 @@ (:SYMBOL "state") (:BECOMES "should be") (:SYMBOL "climax")))) - expected '(fn [cell world] + expected '(fn* ([cell world] (when (= (:state cell) (or (:forest cell) :forest)) - (merge cell {:state :climax}))) + (merge cell {:state :climax})))) actual (generate rule) expected-meta {:rule-type :production} actual-meta (meta actual)]