mw-parser

0.1.0-SNAPSHOT


Parser for production rules for MicroWorld engine

dependencies

org.clojure/clojure
1.5.1
mw-engine
0.1.0-SNAPSHOT



(this space intentionally left almost blank)
 

A very simple parser which parses production rules of the following forms:

  • "if altitude is less than 100 and state is forest then state should be climax and deer should be 3"
  • "if altitude is 100 or fertility is 25 then state should be heath and fertility should be 24.3"
  • "if altitude is 100 or fertility is 25 then state should be heath"
  • "if deer is more than 2 and wolves is 0 and fertility is more than 20 then deer should be deer + 2"
  • "if deer is more than 1 and wolves is more than 1 then deer should be deer - wolves"

It should also but does not yet parse rules of the form:

  • "if 6 neighbours have state is water then state should be fishery"
  • "if state is forest or state is climax and some neighbours have state is fire then 3 in 5 chance that state should be fire"
  • "if state is pasture and more than 3 neighbours have state is scrub then state should be scrub"

it generates rules in the form expected by mw-engine.core

(ns mw-parser.core
  (:use mw-engine.utils
        [clojure.string :only [split triml]]))
(declare parse-conditions)
(declare parse-not-condition)
(declare parse-simple-condition)

a regular expression which matches string representation of numbers

(def re-number #"^[0-9.]*$")

Parse '[property] is less than [value]'.

(defn parse-less-condition
  [[property is less than value & rest]]
  (cond (and (member? is '("is" "are")) (= less "less") (= than "than"))
        [(list '< (list 'get-int 'cell (keyword property)) (read-string value)) rest]))

Parse '[property] is more than [value]'.

(defn parse-more-condition
  [[property is more than value & rest]]
  (cond (and (member? is '("is" "are")) (= more "more") (= than "than"))
        [(list '> (list 'get-int 'cell (keyword property)) (read-string value)) rest]))

Parse clauses of the form 'x is y', but not '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 (and (member? is '("is" "are"))
             (not (member? value '("more" "less" "exactly" "not"))))
        [(cond
          (re-matches re-number value)(list '= (list 'get-int 'cell (keyword property)) (read-string value))
          true (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])))))

Parse conditions of the form '[property] [comparison] [value]'.

(defn parse-simple-condition
  [tokens]
  (or (parse-is-condition tokens)
      (parse-not-condition tokens)
      (parse-less-condition tokens)
      (parse-more-condition tokens)))

Parse '... or [condition]' from tokens, where left is the already parsed first disjunct.

(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 tokens, where left is the already parsed first conjunct.

(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 tokens, where conditions may be linked by either 'and' or 'or'.

(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
  [tokens]
  (if
   (= (first tokens) "if")
   (parse-conditions (rest 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]]
  (if (and (= should "should")
           (= be "be")
           (member? operator '("+" "-" "*" "/")))
    [(list 'merge (or previous 'cell)
           {(keyword prop1) (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]]
  (if (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 the right hand side ('then...') of a production rule.

(defn parse-right-hand-side
  [tokens]
  (if (= (first tokens) "then")
    (parse-actions nil (rest tokens))))

Parse a complete rule from this string or sequence of string tokens.

(defn parse-rule 
  [line]
  (cond
   (string? line) (parse-rule (split (triml line) #"\s+"))
   true (let [[left remainder] (parse-left-hand-side line)
              [right junk] (parse-right-hand-side remainder)]
          ;; there shouldn't be any junk (should be null)
          (list 'fn ['cell 'world] (list 'if left right)))))