dependencies
| (this space intentionally left almost blank) | ||||||||||||
A very simple parser which parses production rules. | (ns ^{:doc :author "Simon Brooke"} mw-parser.declarative (:require [instaparse.core :as insta] [clojure.string :refer [split trim triml]] [mw-parser.errors :as pe] [mw-parser.generate :as pg] [mw-parser.simplify :as ps] [mw-parser.utils :refer [rule?]])) | ||||||||||||
mw-parser: a rule parser for MicroWorld. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Copyright (C) 2014 Simon Brooke | |||||||||||||
(def grammar ;; in order to simplify translation into other natural languages, all ;; TOKENS within the parser should be unambiguous "RULE := IF SPACE CONDITIONS SPACE THEN SPACE ACTIONS; CONDITIONS := DISJUNCT-CONDITION | CONJUNCT-CONDITION | CONDITION ; DISJUNCT-CONDITION := CONDITION SPACE OR SPACE CONDITIONS; CONJUNCT-CONDITION := CONDITION SPACE AND SPACE CONDITIONS; CONDITION := WITHIN-CONDITION | NEIGHBOURS-CONDITION | PROPERTY-CONDITION; WITHIN-CONDITION := QUANTIFIER SPACE NEIGHBOURS SPACE WITHIN SPACE NUMBER SPACE IS SPACE PROPERTY-CONDITION-OR-EXPRESSION; NEIGHBOURS-CONDITION := QUANTIFIER SPACE NEIGHBOURS SPACE IS SPACE PROPERTY-CONDITION | QUALIFIER SPACE NEIGHBOURS-CONDITION; PROPERTY-CONDITION-OR-EXPRESSION := PROPERTY-CONDITION | EXPRESSION; PROPERTY-CONDITION := PROPERTY SPACE QUALIFIER SPACE EXPRESSION | VALUE; EXPRESSION := SIMPLE-EXPRESSION | RANGE-EXPRESSION | NUMERIC-EXPRESSION | DISJUNCT-EXPRESSION | VALUE; SIMPLE-EXPRESSION := QUALIFIER SPACE EXPRESSION | VALUE; DISJUNCT-EXPRESSION := IN SPACE DISJUNCT-VALUE; RANGE-EXPRESSION := BETWEEN SPACE NUMERIC-EXPRESSION SPACE AND SPACE NUMERIC-EXPRESSION; NUMERIC-EXPRESSION := VALUE | VALUE SPACE OPERATOR SPACE NUMERIC-EXPRESSION; NEGATED-QUALIFIER := QUALIFIER SPACE NOT | NOT SPACE QUALIFIER; COMPARATIVE-QUALIFIER := IS SPACE COMPARATIVE SPACE THAN | COMPARATIVE SPACE THAN; QUALIFIER := COMPARATIVE-QUALIFIER | NEGATED-QUALIFIER | EQUIVALENCE | IS SPACE QUALIFIER; QUANTIFIER := NUMBER | SOME | NONE | ALL | COMPARATIVE SPACE THAN SPACE NUMBER; EQUIVALENCE := IS SPACE EQUAL | EQUAL | IS ; COMPARATIVE := MORE | LESS; DISJUNCT-VALUE := VALUE | VALUE SPACE OR SPACE DISJUNCT-VALUE; IF := 'if'; THEN := 'then'; THAN := 'than'; OR := 'or'; NOT := 'not'; AND := 'and'; SOME := 'some'; NONE := 'no'; ALL := 'all' BETWEEN := 'between'; WITHIN := 'within'; IN := 'in'; MORE := 'more' | 'greater'; LESS := 'less' | 'fewer'; OPERATOR := '+' | '-' | '*' | '/'; NEIGHBOURS := 'neighbour' | 'neighbor' | 'neighbours' | 'neighbors'; PROPERTY := SYMBOL; VALUE := SYMBOL | NUMBER; EQUAL := 'equal to'; IS := 'is' | 'are' | 'have' | 'has'; NUMBER := #'[0-9]+' | #'[0-9]+.[0-9]+'; SYMBOL := #'[a-z]+'; ACTIONS := ACTION | ACTION SPACE AND SPACE ACTIONS ACTION := SIMPLE-ACTION | PROBABLE-ACTION; PROBABLE-ACTION := VALUE SPACE CHANCE-IN SPACE VALUE SPACE SIMPLE-ACTION; SIMPLE-ACTION := SYMBOL SPACE BECOMES SPACE EXPRESSION; CHANCE-IN := 'chance in'; BECOMES := 'should be' | 'becomes'; SPACE := #' *'";) | |||||||||||||
Parse the argument, assumed to be a string in the correct syntax, and return a parse tree. | (def parse-rule (insta/parser grammar)) | ||||||||||||
Parse this Throws an exception if parsing fails. | (defn compile-rule ([rule-text return-tuple?] (assert (string? rule-text)) (let [rule (trim rule-text) tree (ps/simplify (parse-rule rule)) afn (if (rule? tree) (eval (pg/generate tree)) ;; else (pe/throw-parse-exception tree))] (if return-tuple? (list afn rule) ;; else afn))) ([rule-text] (compile-rule rule-text false))) | ||||||||||||
Generate Clojure source from simplified parse trees. | (ns ^{:doc :author "Simon Brooke"} mw-parser.generate (:require [mw-engine.utils :refer []] [mw-parser.utils :refer [assert-type TODO]] [mw-parser.errors :as pe])) | ||||||||||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||||||||||||
(declare generate generate-action) | |||||||||||||
From this | (defn generate-rule [tree] (assert-type tree :RULE) (list 'fn ['cell 'world] (list 'if (generate (nth tree 2)) (generate (nth tree 3))))) | ||||||||||||
From this | (defn generate-conditions [tree] (assert-type tree :CONDITIONS) (generate (second tree))) | ||||||||||||
From this | (defn generate-condition [tree] (assert-type tree :CONDITION) (generate (second tree))) | ||||||||||||
(defn generate-conjunct-condition [tree] "From this `tree`, assumed to be a syntactically conjunct correct condition clause, generate and return the appropriate clojure fragment." (assert-type tree :CONJUNCT-CONDITION) (cons 'and (map generate (rest tree)))) | |||||||||||||
From this | (defn generate-disjunct-condition [tree] (assert-type tree :DISJUNCT-CONDITION) (cons 'or (map generate (rest tree)))) | ||||||||||||
From this | (defn generate-ranged-property-condition [tree property expression] (assert-type tree :PROPERTY-CONDITION) (assert-type (nth tree 3) :RANGE-EXPRESSION) (let [l1 (generate (nth expression 2)) l2 (generate (nth expression 4)) pv (list property 'cell)] (list 'let ['lower (list 'min l1 l2) 'upper (list 'max l1 l2)] (list 'and (list '>= pv 'lower)(list '<= pv 'upper))))) | ||||||||||||
From this | (defn generate-disjunct-property-condition ([tree] (let [property (generate (second tree)) qualifier (generate (nth tree 2)) expression (generate (nth tree 3))] (generate-disjunct-property-condition tree property qualifier expression))) ([tree property qualifier expression] (let [e (list 'some (list 'fn ['i] '(= i value)) (list 'quote expression))] (list 'let ['value (list property 'cell)] (if (= qualifier '=) e (list 'not e)))))) | ||||||||||||
From this | (defn generate-property-condition ([tree] (assert-type tree :PROPERTY-CONDITION) (if (and (= (count tree) 2) (= (first (second tree)) :SYMBOL)) ;; it's a shorthand for 'state equal to symbol'. This should probably have ;; been handled in simplify... (generate-property-condition (list :PROPERTY-CONDITION '(:SYMBOL "state") '(:QUALIFIER (:EQUIVALENCE (:EQUAL "equal to"))) (second tree))) ;; otherwise... (generate-property-condition tree (first (nth tree 3))))) ([tree expression-type] (assert-type tree :PROPERTY-CONDITION) (let [property (generate (second tree)) qualifier (generate (nth tree 2)) e (generate (nth tree 3)) expression (cond (and (not (= qualifier '=)) (keyword? e)) (list 'or (list e 'cell) e) (and (not (= qualifier 'not=)) (keyword? e)) (list 'or (list e 'cell) e) :else e)] (case expression-type :DISJUNCT-EXPRESSION (generate-disjunct-property-condition tree property qualifier expression) :RANGE-EXPRESSION (generate-ranged-property-condition tree property expression) (list qualifier (list property 'cell) expression))))) | ||||||||||||
From this | (defn generate-qualifier [tree] (if (= (count tree) 2) (generate (second tree)) ;; else (generate (nth tree 2)))) | ||||||||||||
From this | (defn generate-simple-action ([tree] (assert-type tree :SIMPLE-ACTION) (generate-simple-action tree [])) ([tree others] (assert-type tree :SIMPLE-ACTION) (let [property (generate (second tree)) expression (generate (nth tree 3))] (if (or (= property :x) (= property :y)) (throw (Exception. pe/reserved-properties-error)) (list 'merge (if (empty? others) 'cell ;; else (generate others)) {property expression}))))) | ||||||||||||
From this | (defn generate-probable-action ([tree] (assert-type tree :PROBABLE-ACTION) (generate-probable-action tree [])) ([tree others] (assert-type tree :PROBABLE-ACTION) (let [chances (generate (nth tree 1)) total (generate (nth tree 2)) action (generate-action (nth tree 3) others)] ;; TODO: could almost certainly be done better with macro syntax (list 'if (list '< (list 'rand total) chances) action)))) | ||||||||||||
From this | (defn generate-action [tree others] (case (first tree) :ACTIONS (generate-action (first tree) others) :SIMPLE-ACTION (generate-simple-action tree others) :PROBABLE-ACTION (generate-probable-action tree others) (throw (Exception. (str "Not a known action type: " (first tree)))))) | ||||||||||||
From this | (defn generate-multiple-actions [tree] (assert-type tree :ACTIONS) (generate-action (first (rest tree)) (second (rest tree)))) | ||||||||||||
Generate a disjunct value. Essentially what we need here is to generate a
flat list of values, since the | (defn generate-disjunct-value [tree] (assert-type tree :DISJUNCT-VALUE) (if (= (count tree) 4) (cons (generate (second tree)) (generate (nth tree 3))) (list (generate (second tree))))) | ||||||||||||
From this | (defn generate-numeric-expression [tree] (assert-type tree :NUMERIC-EXPRESSION) (case (count tree) 4 (let [[p operator expression] (rest tree) property (if (number? p) p (list p 'cell))] (list (generate operator) (generate property) (generate expression))) (case (first (second tree)) :SYMBOL (list (keyword (second (second tree))) 'cell) (generate (second tree))))) | ||||||||||||
Generate code for a condition which refers to neighbours. | (defn generate-neighbours-condition ([tree] (assert-type tree :NEIGHBOURS-CONDITION) (case (first (second tree)) :NUMBER (read-string (second (second tree))) :QUANTIFIER (generate-neighbours-condition tree (first (second (second tree)))) :QUALIFIER (cons (generate (second tree)) (rest (generate (nth tree 2)))))) ([tree quantifier-type] (let [quantifier (second tree) pc (generate (nth tree 4))] (case quantifier-type :NUMBER (generate-neighbours-condition '= (read-string (second (second quantifier))) pc 1) :SOME (generate-neighbours-condition '> 0 pc 1) :MORE (let [value (generate (nth quantifier 3))] (generate-neighbours-condition '> value pc 1)) :LESS (let [value (generate (nth quantifier 3))] (generate-neighbours-condition '< value pc 1))))) ([comp1 quantity property-condition distance] (list comp1 (list 'count (list 'remove 'false? (list 'map (list 'fn ['cell] property-condition) (list 'mw-engine.utils/get-neighbours 'world 'cell distance)))) quantity)) ([comp1 quantity property-condition] (generate-neighbours-condition comp1 quantity property-condition 1))) | ||||||||||||
Generate code for a condition which refers to neighbours within a specified distance.
NOTE THAT there's clearly masses of commonality between this and
| (defn generate-within-condition ([tree] (assert-type tree :WITHIN-CONDITION) (case (first (second tree)) :QUANTIFIER (generate-within-condition tree (first (second (second tree)))) :QUALIFIER (TODO "qualified within... help!"))) ([tree quantifier-type] (let [quantifier (second tree) distance (generate (nth tree 4)) pc (generate (nth tree 6))] (case quantifier-type :NUMBER (generate-neighbours-condition '= (read-string (second (second quantifier))) pc distance) :SOME (generate-neighbours-condition '> 0 pc distance) :MORE (let [value (generate (nth quantifier 3))] (generate-neighbours-condition '> value pc distance)) :LESS (let [value (generate (nth quantifier 3))] (generate-neighbours-condition '< value pc distance)))))) | ||||||||||||
Generate code for this (fragment of a) parse tree | (defn generate [tree] (if (coll? tree) (case (first tree) :ACTIONS (generate-multiple-actions tree) :COMPARATIVE (generate (second tree)) :COMPARATIVE-QUALIFIER (generate (second tree)) :CONDITION (generate-condition tree) :CONDITIONS (generate-conditions tree) :CONJUNCT-CONDITION (generate-conjunct-condition tree) :DISJUNCT-CONDITION (generate-disjunct-condition tree) :DISJUNCT-EXPRESSION (generate (nth tree 2)) :DISJUNCT-VALUE (generate-disjunct-value tree) :EQUIVALENCE '= :EXPRESSION (generate (second tree)) :LESS '< :MORE '> :NEGATED-QUALIFIER (case (generate (second tree)) = 'not= > '< < '>) :NEIGHBOURS-CONDITION (generate-neighbours-condition tree) :NUMERIC-EXPRESSION (generate-numeric-expression tree) :NUMBER (read-string (second tree)) :OPERATOR (symbol (second tree)) :PROBABLE-ACTION (generate-probable-action tree) :PROPERTY (list (generate (second tree)) 'cell) ;; dubious - may not be right :PROPERTY-CONDITION (generate-property-condition tree) :QUALIFIER (generate-qualifier tree) :RULE (generate-rule tree) :SIMPLE-ACTION (generate-simple-action tree) :SYMBOL (keyword (second tree)) :VALUE (generate (second tree)) :WITHIN-CONDITION (generate-within-condition tree) (map generate tree)) tree)) | ||||||||||||
Display parse errors in a format which makes it easy for the user to see where the error occurred. | (ns ^{:doc :author "Simon Brooke"} mw-parser.errors) | ||||||||||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||||||||||||
error thrown when an attempt is made to set a reserved property | (def reserved-properties-error "The properties 'x' and 'y' of a cell are reserved and should not be set in rule actions") | ||||||||||||
error thrown when a rule cannot be parsed. Slots are for (1) rule text (2) cursor showing where in the rule text the error occurred (3) the reason for the error | (def bad-parse-error "I did not understand:\n '%s'\n %s\n %s") | ||||||||||||
Attempt to explain the reason for the parse error. | (defn- explain-parse-error-reason [reason] (str "Expecting one of (" (apply str (map #(str (:expecting %) " ") reason)) ")")) | ||||||||||||
(defn- parser-error-to-map [parser-error] (let [m (reduce (fn [map item](merge map {(first item)(second item)})) {} parser-error) reason (map #(reduce (fn [map item] (merge {(first item) (second item)} map)) {} %) (:reason m))] (merge m {:reason reason}))) | |||||||||||||
Construct a helpful error message from this | (defn throw-parse-exception [parser-error] (assert (coll? parser-error) "Expected a paser error structure?") (let [ ;; the error structure is a list, such that each element is a list of two items, and ;; the first element in each sublist is a keyword. Easier to work with it as a map error-map (parser-error-to-map parser-error) text (:text error-map) reason (explain-parse-error-reason (:reason error-map)) ;; rules have only one line, by definition; we're interested in the column column (if (:column error-map)(:column error-map) 0) ;; create a cursor to point to that column cursor (apply str (reverse (conj (repeat column " ") "^"))) message (format bad-parse-error text cursor reason) ] (throw (Exception. message)))) | ||||||||||||
A very simple parser which parses production rules. | (ns ^{:doc :author "Simon Brooke"} mw-parser.core (:use mw-engine.utils [clojure.string :only [split trim triml]]) (:gen-class)) | ||||||||||||
mw-parser: a rule parser for MicroWorld. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Copyright (C) 2014 Simon Brooke A very simple parser which parses production rules of the following forms:
it generates rules in the form expected by It is, as I say, very simple; it generates a complete rule, or it fails completely, returning nil. Very occasionally it generates a wrong rule - one which is not a correct translation of the rule semantics - but that is buggy behaviour, which I'll try to fix over the next few weeks, not a design fault. More significantly it does not generate useful error messages on failure. This parser is now obsolete, but is retained in the codebase for now in case it is of use to anyone. Prefer the declarative.clj parser. | |||||||||||||
(declare parse-conditions) (declare parse-not-condition) (declare parse-simple-condition) | |||||||||||||
a regular expression which matches string representation of positive numbers | (def re-number #"^[0-9.]*$") | ||||||||||||
error thrown when an attempt is made to set a reserved property | (def reserved-properties-error "The properties 'x' and 'y' of a cell are reserved and should not be set in rule actions") | ||||||||||||
error thrown when a rule cannot be parsed | (def bad-parse-error "I did not understand '%s'") | ||||||||||||
If this token appears to represent an explicit number, return that number; otherwise, make a keyword of it and return that. | (defn- keyword-or-numeric [token] (cond (re-matches re-number token) (read-string token) (keyword? token) token true (keyword token))) | ||||||||||||
Generally all functions in this file with names beginning 'parse-' take a sequence of tokens (and in some cases other optional arguments) and return a vector comprising
In every case if the function cannot parse the desired construct from the front of the sequence of tokens it returns nil. | |||||||||||||
Parse a number. | (defn parse-numeric-value [[value & remainder]] (if (and value (re-matches re-number value)) [(read-string value) remainder])) | ||||||||||||
Parse a token assumed to be the name of a property of the current cell, whose value is assumed to be an integer. | (defn parse-property-int [[value & remainder]] (if value [(list 'get-int 'cell (keyword value)) remainder])) | ||||||||||||
Parse a token assumed to be the name of a property of the current cell. | (defn parse-property-value [[value & remainder]] (if value [(list (keyword value) 'cell) remainder])) | ||||||||||||
Parse a token assumed to be a simple token value. | (defn parse-token-value [[value & remainder]] (if value [(keyword value) remainder])) | ||||||||||||
Parse a value from the first of these | (defn parse-simple-value ([tokens expect-int] (or (parse-numeric-value tokens) (cond expect-int (parse-property-int tokens) true (parse-token-value tokens)))) ([tokens] (parse-simple-value tokens false))) | ||||||||||||
Parse a single value from this single token and return just the generated code, not a pair. | (defn gen-token-value [token expect-int] (first (parse-simple-value (list token) expect-int))) | ||||||||||||
Parse a list of values from among these | (defn parse-disjunct-value [[OR token & tokens] expect-int] (cond (member? OR '("or" "in")) (let [value (first (parse-simple-value (list token) expect-int)) seek-others (= (first tokens) "or")] (cond seek-others (let [[others remainder] (parse-disjunct-value tokens expect-int)] [(cons value others) remainder]) true [(list value) tokens])))) | ||||||||||||
Parse a value from among these | (defn parse-value ([tokens expect-int] (or (parse-disjunct-value tokens expect-int) (parse-simple-value tokens expect-int))) ([tokens] (parse-value tokens false))) | ||||||||||||
Parses a condition of the form '[property] in [value] or [value]...' | (defn parse-member-condition [[property IS IN & rest]] (if (and (member? IS '("is" "are")) (= IN "in")) (let [[l remainder] (parse-disjunct-value (cons "in" rest) false)] [(list 'member? (list (keyword property) 'cell) (list 'quote l)) remainder]))) | ||||||||||||
Parse '[property] less than [value]'. | (defn- parse-less-condition [[property IS LESS THAN & rest]] (cond (and (member? IS '("is" "are")) (member? LESS '("less" "fewer")) (= THAN "than")) (let [[value remainder] (parse-value rest true)] [(list '< (list 'get-int 'cell (keyword property)) value) remainder]))) | ||||||||||||
Parse '[property] more than [value]'. | (defn- parse-more-condition [[property IS MORE THAN & rest]] (cond (and (member? IS '("is" "are")) (member? MORE '("more" "greater")) (= THAN "than")) (let [[value remainder] (parse-value rest true)] [(list '> (list 'get-int 'cell (keyword property)) value) remainder]))) | ||||||||||||
(defn- parse-between-condition [[p IS BETWEEN v1 AND v2 & rest]] (cond (and (member? IS '("is" "are")) (= BETWEEN "between") (= AND "and") (not (nil? v2))) (let [property (first (parse-simple-value (list p) true)) value1 (first (parse-simple-value (list v1) true)) value2 (first (parse-simple-value (list v2) true))] [(list 'or (list '< value1 property value2) (list '> value1 property value2)) rest]))) | |||||||||||||
Parse clauses of the form 'x is y', 'x is in y or z...', 'x is between y and z', 'x is more than y' or 'x is less than y'. It is necessary to disambiguate whether value is a numeric or keyword. | (defn- parse-is-condition [[property IS value & rest]] (cond (member? IS '("is" "are")) (let [tokens (cons property (cons value rest))] (cond (re-matches re-number value) [(list '= (list 'get-int 'cell (keyword property)) (read-string value)) rest] value [(list '= (list (keyword property) 'cell) (keyword value)) rest])))) | ||||||||||||
Parse the negation of a simple condition. | (defn- parse-not-condition [[property IS NOT & rest]] (cond (and (member? IS '("is" "are")) (= NOT "not")) (let [partial (parse-simple-condition (cons property (cons "is" rest)))] (cond partial (let [[condition remainder] partial] [(list 'not condition) remainder]))))) | ||||||||||||
(defn- gen-neighbours-condition ([comp1 quantity property value remainder comp2 distance] [(list comp1 (list 'count (list 'get-neighbours-with-property-value 'world '(cell :x) '(cell :y) distance (keyword property) (keyword-or-numeric value) comp2)) quantity) remainder]) ([comp1 quantity property value remainder comp2] (gen-neighbours-condition comp1 quantity property value remainder comp2 1))) | |||||||||||||
Parse conditions of the form '...more than 6 neighbours are [condition]' | (defn parse-comparator-neighbours-condition [[MORE THAN n NEIGHBOURS WITHIN distance have-or-are & rest]] (let [quantity (first (parse-numeric-value (list n))) comparator (cond (= MORE "more") '> (member? MORE '("fewer" "less")) '<)] (cond (not= WITHIN "within") (parse-comparator-neighbours-condition (flatten ;; two tokens were mis-parsed as 'within distance' that weren't ;; actually 'within' and a distance. Splice in 'within 1' and try ;; again. (list MORE THAN n NEIGHBOURS "within" "1" WITHIN distance have-or-are rest))) (and quantity comparator (= THAN "than") (= NEIGHBOURS "neighbours")) (cond (= have-or-are "are") (let [[value & remainder] rest dist (gen-token-value distance true)] (gen-neighbours-condition comparator quantity :state value remainder = dist)) (= have-or-are "have") (let [[property comp1 comp2 value & remainder] rest dist (gen-token-value distance true)] (cond (and (= comp1 "equal") (= comp2 "to")) (gen-neighbours-condition comparator quantity property value remainder = dist) (and (= comp1 "more") (= comp2 "than")) (gen-neighbours-condition comparator quantity property value remainder > dist) (and (= comp1 "less") (= comp2 "than")) (gen-neighbours-condition comparator quantity property value remainder < dist))))))) | ||||||||||||
(defn parse-some-neighbours-condition [[SOME NEIGHBOURS & rest]] (cond (and (= SOME "some") (= NEIGHBOURS "neighbours")) (parse-comparator-neighbours-condition (concat '("more" "than" "0" "neighbours") rest)))) | |||||||||||||
Parse conditions of the form '...6 neighbours are [condition]' | (defn parse-simple-neighbours-condition [[n NEIGHBOURS WITHIN distance have-or-are & rest]] (let [quantity (first (parse-numeric-value (list n)))] (cond (and quantity (= NEIGHBOURS "neighbours")) (cond (not= WITHIN "within") (parse-simple-neighbours-condition (flatten ;; two tokens were mis-parsed as 'within distance' that weren't ;; actually 'within' and a distance. Splice in 'within 1' and try ;; again. (list n NEIGHBOURS "within" "1" WITHIN distance have-or-are rest))) (= have-or-are "are") (let [[value & remainder] rest dist (gen-token-value distance true)] (gen-neighbours-condition '= quantity :state value remainder = dist)) (= have-or-are "have") (let [[property comp1 comp2 value & remainder] rest dist (gen-token-value distance true)] (cond (and (= comp1 "equal") (= comp2 "to")) (gen-neighbours-condition '= quantity property value remainder = dist) (and (= comp1 "more") (= comp2 "than")) (gen-neighbours-condition '= quantity property value remainder > dist) (and (= comp1 "less") (= comp2 "than")) (gen-neighbours-condition '= quantity property value remainder < dist))))))) | ||||||||||||
Parse conditions referring to neighbours | (defn parse-neighbours-condition [tokens] (or (parse-simple-neighbours-condition tokens) (parse-comparator-neighbours-condition tokens) (parse-some-neighbours-condition tokens))) | ||||||||||||
Parse conditions of the form '[property] [comparison] [value]'. | (defn parse-simple-condition [tokens] (or (parse-neighbours-condition tokens) (parse-member-condition tokens) (parse-not-condition tokens) (parse-less-condition tokens) (parse-more-condition tokens) (parse-between-condition tokens) (parse-is-condition tokens))) | ||||||||||||
Parse '... or [condition]' from | (defn- parse-disjunction-condition [left tokens] (let [partial (parse-conditions tokens)] (if partial (let [[right remainder] partial] [(list 'or left right) remainder])))) | ||||||||||||
Parse '... and [condition]' from | (defn- parse-conjunction-condition [left tokens] (let [partial (parse-conditions tokens)] (if partial (let [[right remainder] partial] [(list 'and left right) remainder])))) | ||||||||||||
Parse conditions from | (defn- parse-conditions [tokens] (let [partial (parse-simple-condition tokens)] (if partial (let [[left [next & remainder]] partial] (cond (= next "and") (parse-conjunction-condition left remainder) (= next "or") (parse-disjunction-condition left remainder) true partial))))) | ||||||||||||
Parse the left hand side ('if...') of a production rule. | (defn- parse-left-hand-side [[IF & tokens]] (if (= IF "if") (parse-conditions tokens))) | ||||||||||||
Parse actions of the form '[property] should be [property] [arithmetic-operator] [value]', e.g. 'fertility should be fertility + 1', or 'deer should be deer - wolves'. | (defn- parse-arithmetic-action [previous [prop1 SHOULD BE prop2 operator value & rest]] (cond (member? prop1 '("x" "y")) (throw (Exception. reserved-properties-error)) (and (= SHOULD "should") (= BE "be") (member? operator '("+" "-" "*" "/"))) [(list 'merge (or previous 'cell) {(keyword prop1) (list 'int (list (symbol operator) (list 'get-int 'cell (keyword prop2)) (cond (re-matches re-number value) (read-string value) true (list 'get-int 'cell (keyword value)))))}) rest])) | ||||||||||||
Parse actions of the form '[property] should be [value].' | (defn- parse-set-action [previous [property SHOULD BE value & rest]] (cond (member? property '("x" "y")) (throw (Exception. reserved-properties-error)) (and (= SHOULD "should") (= BE "be")) [(list 'merge (or previous 'cell) {(keyword property) (cond (re-matches re-number value) (read-string value) true (keyword value))}) rest])) | ||||||||||||
(defn- parse-simple-action [previous tokens] (or (parse-arithmetic-action previous tokens) (parse-set-action previous tokens))) | |||||||||||||
Parse actions from tokens. | (defn- parse-actions [previous tokens] (let [[left remainder] (parse-simple-action previous tokens)] (cond left (cond (= (first remainder) "and") (parse-actions left (rest remainder)) true (list left))))) | ||||||||||||
Parse a probability of an action from this collection of tokens | (defn- parse-probability [previous [n CHANCE IN m & tokens]] (cond (and (= CHANCE "chance")(= IN "in")) (let [[action remainder] (parse-actions previous tokens)] (cond action [(list 'cond (list '< (list 'rand (first (parse-simple-value (list m) true))) (first (parse-simple-value (list n) true))) action) remainder])))) | ||||||||||||
Parse the right hand side ('then...') of a production rule. | (defn- parse-right-hand-side [[THEN & tokens]] (if (= THEN "then") (or (parse-probability nil tokens) (parse-actions nil tokens)))) | ||||||||||||
Parse a complete rule from this Throws an exception if parsing fails. | (defn parse-rule [line] (cond (string? line) (let [rule (parse-rule (split (triml line) #"\s+"))] (cond rule rule true (throw (Exception. (format bad-parse-error line))))) true (let [[left remainder] (parse-left-hand-side line) [right junk] (parse-right-hand-side remainder)] (cond ;; there should be a valide left hand side and a valid right hand side ;; there shouldn't be anything left over (junk should be empty) (and left right (empty? junk)) (list 'fn ['cell 'world] (list 'if left right)))))) | ||||||||||||
Parse this Throws an exception if parsing fails. | (defn compile-rule ([rule-text return-tuple?] (do (use 'mw-engine.utils) (let [afn (eval (parse-rule rule-text))] (cond (and afn return-tuple?)(list afn (trim rule-text)) true afn)))) ([rule-text] (compile-rule rule-text false))) | ||||||||||||
parse multiple rules from a stream, possibly a file. | (ns ^{:doc :author "Simon Brooke"} mw-parser.bulk (:use mw-parser.core mw-engine.utils clojure.java.io [clojure.string :only [split trim]]) (:import (java.io BufferedReader StringReader))) | ||||||||||||
mw-parser: a rule parser for MicroWorld. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Copyright (C) 2014 Simon Brooke | |||||||||||||
Is this | (defn comment? [line] (or (empty? (trim line)) (member? (first line) '(nil \# \;)))) | ||||||||||||
Parse rules from successive lines in this | (defn parse-string [string] ;; TODO: tried to do this using with-open, but couldn't make it work. (map #(parse-rule (trim %)) (remove comment? (split string #"\n")))) | ||||||||||||
Parse rules from successive lines in the file loaded from this | (defn parse-file [filename] (parse-string (slurp filename))) | ||||||||||||
Compile each non-comment line of this | (defn compile-string [string] (map #(compile-rule % true) (remove comment? (split string #"\n")))) | ||||||||||||
Compile each non-comment line of the file indicated by this | (defn compile-file [filename] (compile-string (slurp filename))) | ||||||||||||
Simplify a parse tree. | (ns ^{:doc :author "Simon Brooke"} mw-parser.simplify (:require [mw-engine.utils :refer [member?]])) | ||||||||||||
mw-parser: a rule parser for MicroWorld. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Copyright (C) 2014 Simon Brooke | |||||||||||||
(declare simplify) | |||||||||||||
Given that this | (defn simplify-qualifier [tree] (cond (empty? tree) nil (and (coll? tree) (member? (first tree) '(:EQUIVALENCE :COMPARATIVE))) tree (coll? (first tree)) (or (simplify-qualifier (first tree)) (simplify-qualifier (rest tree))) (coll? tree) (simplify-qualifier (rest tree)) true tree)) | ||||||||||||
There are a number of possible simplifications such that if the | (defn simplify-second-of-two [tree] (if (= (count tree) 2) (simplify (nth tree 1)) tree)) | ||||||||||||
If this quantifier is a number, 'simplifiy' it into a comparative whose operator is '=' and whose quantity is that number. This is actually more complicated but makes generation easier. | (defn simplify-quantifier [tree] (if (number? (second tree)) [:COMPARATIVE '= (second tree)] (simplify (second tree)))) | ||||||||||||
Simplify/canonicalise this | (defn simplify [tree] (if (coll? tree) (case (first tree) :ACTION (simplify-second-of-two tree) :ACTIONS (cons (first tree) (simplify (rest tree))) :CHANCE-IN nil :COMPARATIVE (simplify-second-of-two tree) :CONDITION (simplify-second-of-two tree) :CONDITIONS (simplify-second-of-two tree) :EXPRESSION (simplify-second-of-two tree) :PROPERTY (simplify-second-of-two tree) :PROPERTY-CONDITION-OR-EXPRESSION (simplify-second-of-two tree) :SPACE nil :THEN nil :AND nil :VALUE (simplify-second-of-two tree) (remove nil? (map simplify tree))) tree)) | ||||||||||||
Utilities used in more than one namespace within the parser. | (ns ^{:doc :author "Simon Brooke"} mw-parser.utils) | ||||||||||||
mw-parser: a rule parser for MicroWorld. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Copyright (C) 2014 Simon Brooke | |||||||||||||
Return true if the argument appears to be a parsed rule tree, else false. | (defn rule? [maybe-rule] (and (coll? maybe-rule) (= (first maybe-rule) :RULE))) | ||||||||||||
Marker to indicate I'm not yet finished! | (defn TODO [message] message) | ||||||||||||
Return | (defn suitable-fragment? [tree-fragment type] (and (coll? tree-fragment) (= (first tree-fragment) type))) | ||||||||||||
If | (defn assert-type [tree-fragment type] (assert (suitable-fragment? tree-fragment type) (throw (Exception. (format "Expected a %s fragment" type))))) | ||||||||||||
Return the first element of this tree which has this tag in a depth-first, left-to-right search | (defn search-tree [tree tag] (cond (= (first tree) tag) tree :else (first (remove nil? (map #(search-tree % tag) (rest tree)))))) | ||||||||||||