Search code examples
clojureinstaparse

How to traverse parse tree from instaparse


I'm experimenting with Clojure and Instaparse. I have created a small toy language, and I'm getting stuck at how to properly treat the resulting tree. This is what i get:

[:ClassDescription 
 [:ClassName "Test"]
 [:Properties 
  [:Property 
   [:PropertyName "ID"] 
   [:PropertyType "Int"]] 
  [:Property 
   [:PropertyName "Name"] 
   [:PropertyType "string"]]]]

Now, as an example I would like to extract all PropertyTypes. I have two main ways and I'd like a solution for both.

  1. By specifying a path; something like [:ClassDescription :Properties :Property :PropertyType]
  2. By extracting all :PropertyType elements, regardless of depth.

For A., my first idea was to convert some parts of it to maps via insta/transform and then use get-in, but then I get a really clunky solution involving nested loops and get-in.

I can also use nth, and drill my self into the structure, but that seems cumbersome, and will break easily if I add another layer.

My other idea was a recursive solution where I treat every element the same way and loop through it and check for all matches.

For B. my only solution so far is a recursive function that just drills through everything and tries to match the first element.

I believe that these "handwritten" functions could be avoided by some clever combination of insta/transform, map, filter, reduce, etc. Can it?


Solution

  • For getting elements out of the parsed data, you could use tree-seq to iterate all and pick what you need. E.g.:

    (defn parsed-tree-seq [parsed]
      (tree-seq #(vector? (second %)) rest parsed))
    
    (map second
         (filter
           #(= (first %) :PropertyType)
           (parsed-tree-seq parsed)))
    ; => ("Int" "string")
    

    Yet you might be better off to shape your data already in the first place by using <...> in your parser and/or by turning them into something more mapish, what would be easier to access via transformation. E.g. turn the :Properties into a list of maps and turn the whole thing into a map:

    (defn transform [parsed]
      (insta/transform
        {:Property #(into {} %&)
         :Properties #(vec (concat [:Properties] [%&]))
         :ClassDescription #(into {} %&)
         } parsed))
    
    (map :PropertyType (:Properties (transform parsed)))
    ; => ("Int" "string")