postwalk to evaluate arithmetic expression

444 views Asked by At

I am trying to use Instaparse to make a simple arithmetic expression evaluator. The parser seems to work fine but I cannot figure out how to evaluate the returned nested vector. Currently I am using postwalk, like this

(ns test5.core
  (:require [instaparse.core :as insta])
  (:require [clojure.walk  :refer [postwalk]])
  (:gen-class))


(def WS
  (insta/parser
    "WS = #'\\s+'"))


(def transform-options
  {:IntLiteral read-string})


(def parser
  (insta/parser
    "AddExpr = AddExpr '+' MultExpr
      | AddExpr '-' MultExpr
      | MultExpr

     MultExpr = MultExpr '*' IntLiteral
      | MultExpr '/' IntLiteral
      | IntLiteral

     IntLiteral = #'[0-9]+'"

    :auto-whitespace WS))


(defn parse[input]
  (->> (parser input)
       (insta/transform transform-options)))


(defn visit [node]
  (println node)
  (cond
    (number? node) node
    (string? node) (resolve (symbol node))
    (vector? node)
      (cond
        (= :MultExpr (first node)) (visit (rest node))
        (= :AddExpr (first node)) (visit (rest node))
        :else node)
    :else node))


(defn evaluate [tree]
  (println tree)
  (postwalk visit tree))


(defn -main
  [& args]
  (evaluate (parse "1 * 2 + 3")))

postwalk does traverse the vector but I get a nested list as the result, eg

((((1) #'clojure.core/* 2)) #'clojure.core/+ (3))
3

There are 3 answers

0
rmcv On BEST ANSWER

Use org.clojure/core.match. Base on your current grammar, you can write the evaluation function as:

(defn eval-expr [expr]
  (match expr
         [:MultExpr e1 "*" e2] (* (eval-expr e1)
                                  (eval-expr e2))
         [:MultExpr e1 "/" e2] (/ (eval-expr e1)
                                  (eval-expr e2))
         [:AddExpr e1 "+" e2] (+ (eval-expr e1)
                                 (eval-expr e2))
         [:AddExpr e1 "-" e2] (- (eval-expr e1)
                                 (eval-expr e2))
         [:MultExpr e1] (eval-expr e1)
         [:AddExpr e1] (eval-expr e1)
         :else expr))

and evaluate with:

(-> "1 * 2 + 3"
    parse
    eval-expr)
;; => 5

0
Alan Thompson On

This exact problem is why I first created the Tupelo Forest library.

Please see the talk from Clojure Conj 2017.

I've started some docs here. You can also see live examples here.


Update

Here is how you could use the Tupelo Forest library to do it:

First, define your Abstract Syntax Tree (AST) data using Hiccup format:

  (with-forest (new-forest)
    (let [data-hiccup      [:rpc
                            [:fn {:type :+}
                             [:value 2]
                             [:value 3]]]
          root-hid         (add-tree-hiccup data-hiccup)

with result:

(hid->bush root-hid) => 
[{:tag :rpc}
 [{:type :+, :tag :fn}
  [{:tag :value, :value 2}]
  [{:tag :value, :value 3}]]]

Show how walk-tree works using a "display interceptor"

  disp-interceptor {:leave (fn [path]
                             (let [curr-hid  (xlast path)
                                   curr-node (hid->node curr-hid)]
                               (spyx curr-node)))}
  >>        (do
              (println "Display walk-tree processing:")
              (walk-tree root-hid disp-interceptor))

with result:

Display walk-tree processing:
curr-node => {:tupelo.forest/khids [], :tag :value, :value 2}
curr-node => {:tupelo.forest/khids [], :tag :value, :value 3}
curr-node => {:tupelo.forest/khids [1037 1038], :type :+, :tag :fn}
curr-node => {:tupelo.forest/khids [1039], :tag :rpc}

then define the operators and an interceptor to transform a subtree like (+ 2 3) => 5

  op->fn           {:+ +
                    :* *}
  math-interceptor {:leave (fn [path]
                             (let [curr-hid  (xlast path)
                                   curr-node (hid->node curr-hid)
                                   curr-tag  (grab :tag curr-node)]
                               (when (= :fn curr-tag)
                                 (let [curr-op    (grab :type curr-node)
                                       curr-fn    (grab curr-op op->fn)
                                       kid-hids   (hid->kids curr-hid)
                                       kid-values (mapv hid->value kid-hids)                                           
                                       result-val (apply curr-fn kid-values)]
                                   (set-node curr-hid {:tag :value :value result-val} [])))))}  
  ]  ; end of let form

; imperative step replaces old nodes with result of math op
(walk-tree root-hid math-interceptor)

We can then display the modified AST tree which contains the result of (+ 2 3):

(hid->bush root-hid) => 
[{:tag :rpc} 
  [{:tag :value, :value 5}]]

You can see the live code here.

1
Taylor Wood On

This doesn't use Instaparse or clojure.walk, but here's something I had for evaluating infix math using only reduce:

(defn evaluate
  "Evaluates an infix arithmetic form e.g. (1 + 1 * 2)."
  [e]
  (let [eval-op (fn [op a b]
                  (let [f (resolve op)]
                    (f a b)))]
    (reduce
      (fn [[v op] elem]
        (cond
          (coll? elem)
          (if op
            [(eval-op op v (first (evaluate elem))) nil]
            [(first (evaluate elem)) nil])

          (and op (number? elem))
          [(eval-op op v elem) nil]

          (number? elem)
          [elem nil]

          (symbol? elem)
          [v elem]

          :else
          (throw (ex-info "Invalid evaluation" {:v v :op op :elem (type elem)}))))
      [0 nil]
      e)))

(first (evaluate (clojure.edn/read-string "(1 * 2 + 3)")))
=> 5
(first (evaluate (clojure.edn/read-string "(1 * 2 + (3 * 5))")))
=> 17

This requires the input string to represent a valid Clojure list. I also had this function for grouping multiplication/division:

(defn pemdas
  "Groups division/multiplication operations in e into lists."
  [e]
  (loop [out []
         rem e]
    (if (empty? rem)
      (seq out)
      (let [curr (first rem)
            next' (second rem)]
        (if (contains? #{'/ '*} next')
          (recur (conj out (list curr next' (nth rem 2)))
                 (drop 3 rem))
          (recur (conj out curr) (rest rem)))))))

(pemdas '(9.87 + 4 / 3 * 0.41))
=> (9.87 + (4 / 3) * 0.41)