Added protection against null pointer exceptions in numeric properties.

This commit is contained in:
Simon Brooke 2023-07-21 23:52:04 +01:00
parent b4f796aca4
commit a436499d98
4 changed files with 73 additions and 15 deletions

2
.gitignore vendored
View file

@ -9,3 +9,5 @@ pom.xml
.nrepl-port
doc/scratch.clj
src/mw_parser/scratch.clj

View file

@ -13,15 +13,49 @@ You can see MicroWorld in action [here](http://www.journeyman.cc/microworld/) -
but please don't be mean to my poor little server. If you want to run big maps
or complex rule-sets, please run it on your own machines.
### Version compatibility
There are substantial changes in how rule functions are evaluated between 0.1.x
versions of MicroWorld libraries and 0.3.x versions. In particular, in 0.3.x
metadata is held on rule functions which is essential to the functioning of the
engine. Consequently, you cannot mix 0.1.x and 0.3.x libraries: it will not work.
In particular the parser in actual use has changed in 0.3.x from the
`mw-parser.core` parser to the `mw-parser.declarative` parser. The API of the
parser is also substantially revised and is not backwards compatible, so if
you have code written to use 0.1.x versions of this library it will need to be
modified. I apologise for this. On the upside, the new parser API is much
simpler.
## Usage
Main entry point is (parse-rule _string_), where string takes a form detailed
in __[grammar](#grammar)__, below. If the rule is interpretted correctly the result will
be the source code of a Clojure anonymous function; if the rule cannot be interpretted,
an error 'I did not understand...' will be shown.
Main entry point is (compile _string_), where string takes a form detailed
in __[grammar](#grammar)__, below. If the rules represnted by the string are
interpretted correctly, the result will be a a list of compiled Clojure
anonymous functions; if the rule cannot be interpretted, an error 'I did not
understand...' will be thrown.
The function (compile-rule _string_) is like parse-rule, except that it returns
a compiled Clojure anonymous function.
Each of these functions will have metadata including:
* `:rule-type` : the type of rule the function represents;
* `:lisp` : the lisp source from which the function was compiled;
* `:parse` : the parse-tree from which that lisp source was derived;
* `:source` : the rule source from which the parse-tree was derived;
* `:line : the one-based line number of the rule source in the source file.
The values of `:rule-type` currently supported are:
* `:production` : an if-then rule which transforms the properties of a single
cell, based on the values of properties of that cell and optionally of its
neighbours;
* `:flow` : a flow rule which creates flows of values of a numeric property
from one cell to other cells.
Values which it is intended will be supported include rules to create graphs
which will enable the user to aggregate and interpret what is happening in
the world. Types which it is envisaged will be supported include
`:time-series`, `bar-graph` and perhaps others, but grammar for these has not
yet been developed.
### Generated function and evaluation environment

View file

@ -32,6 +32,11 @@
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; TODO: Either, when I first wrote this parser, I didn't adequately read the
;;; Instaparse documentation, or Instaparse has advanced considerably since
;;; then. Reading the documentation now, I could probably rewrite this to
;;; eliminate the simplify step altogether, and that would be well worth doing.
(def ruleset-grammar
"Experimental: parse a whole file in one go."
(join "\n" ["LINES := LINE | LINE CR LINES;"

View file

@ -1,7 +1,9 @@
(ns ^{:doc "Generate Clojure source from simplified parse trees."
:author "Simon Brooke"}
mw-parser.generate
(:require [mw-parser.utils :refer [assert-type search-tree TODO]]))
mw-parser.generate
(:require
[mw-engine.utils :refer :all] ;; may need these when macro-expanding rules.
[mw-parser.utils :refer [assert-type search-tree TODO]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
@ -36,7 +38,7 @@
generate and return the appropriate rule as a function of two arguments."
[tree]
(assert-type tree :RULE)
(vary-meta
(vary-meta
;; do macro-expansion here, because at least in theory I know what
;; macros are in scope here.
(macroexpand
@ -131,7 +133,9 @@
(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)))))
(list qualifier (if (number? expression)
(list 'mw-engine.utils/get-num 'cell property)
(list property 'cell)) expression)))))
(defn generate-qualifier
"From this `tree`, assumed to be a syntactically correct qualifier,
@ -161,6 +165,21 @@
(generate others))
{property expression})))))
(defn trap-errors-in-dice-throw
"We're getting a wierd -- many would say 'impossible' -- intermittent bug
which appears to happen here. "
[sides chances action]
;; (list 'try
(list 'if (list '< (list 'rand sides) chances) action)
;; (list 'catch 'Exception 'any
;; (list 'println (list 'format "Dice throw bug %d/%d" chances sides))
;; (list 'throw (list 'ex-info "Error in dice throw"
;; {:total sides
;; :chances chances
;; :action action}
;; 'any))))
)
(defn generate-probable-action
"From this `tree`, assumed to be a syntactically correct probable action,
generate and return the appropriate clojure fragment."
@ -174,9 +193,7 @@
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))))
(trap-errors-in-dice-throw total chances action))))
(defn generate-action
"From this `tree`, assumed to be a syntactically correct action,
@ -211,10 +228,10 @@
(assert-type tree :NUMERIC-EXPRESSION)
(case (count tree)
4 (let [[p operator expression] (rest tree)
property (if (number? p) p (list p 'cell))]
property (if (number? p) p (list 'mw-engine.utils/get-num 'cell p))]
(list (generate operator) (generate property) (generate expression)))
(case (first (second tree))
:SYMBOL (list (keyword (second (second tree))) 'cell)
:SYMBOL (list 'mw-engine.utils/get-num 'cell (generate (second tree)))
(generate (second tree)))))
(defn generate-neighbours-condition