From a436499d981ba9d8a8dcc0c7476ce183fcad69b3 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 21 Jul 2023 23:52:04 +0100 Subject: [PATCH] Added protection against null pointer exceptions in numeric properties. --- .gitignore | 2 ++ README.md | 46 ++++++++++++++++++++++++++++++----- src/mw_parser/declarative.clj | 5 ++++ src/mw_parser/generate.clj | 35 +++++++++++++++++++------- 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index b0f5637..06a11d9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ pom.xml .nrepl-port doc/scratch.clj + +src/mw_parser/scratch.clj diff --git a/README.md b/README.md index 76f662d..45fd595 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/mw_parser/declarative.clj b/src/mw_parser/declarative.clj index 5e121db..69bda0d 100644 --- a/src/mw_parser/declarative.clj +++ b/src/mw_parser/declarative.clj @@ -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;" diff --git a/src/mw_parser/generate.clj b/src/mw_parser/generate.clj index 8cc4580..700053c 100644 --- a/src/mw_parser/generate.clj +++ b/src/mw_parser/generate.clj @@ -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