2013-08-12 16 views
8

Jestem początkującym do analizowania i chcę przeanalizować niektóre kody clojure. Mam nadzieję, że ktoś może podać przykład tego, jak można analizować kod clojure za pomocą instaparse. Potrzebuję tylko cyfr, symboli, słów kluczowych, sexów, wektorów i białych znaków.Jak definiujemy gramatykę dla kodu typu clojure za pomocą instaparse?

Niektóre przykłady, które chciałbym analizować:

(+ 1 2 
    (+ 3 4)) 

{:hello "there" 
:look '(i am 
      indented)} 
+0

Instaparse jest naprawdę bogaty https://github.com/Engelberg/instaparse – Chiron

+0

Czy możesz podać przykład pożądanego rezultatu? –

+0

może [: sexp [: sym +] [: num 1] [: num 2] [: sexp [: sym +] [: num 3] [: num 4]]] ?? Naprawdę nie wiem ... ma to związek z tym pytaniem: http://stackoverflow.com/questions/18184834/how-would-you-get-the-clojure-reader-to-keep-formatowanie – zcaudate

Odpowiedz

22

Dobrze istnieją dwie części do Twojego pytania. Pierwsza część analizuje wyrażenie. W drugiej części przekształcane jest dane wyjściowe do pożądanego wyniku. Aby uzyskać dobre zrozumienie tych zasad, bardzo polecam również Udville'a Programming Languages course. Carin Meier's blog post.

Najlepszym sposobem zrozumienia działania parsera jest rozbicie go na mniejsze części. Tak więc w pierwszej kolejności przeanalizujemy tylko niektóre zasady parsowania, aw drugiej części będziemy budować nasze seksu.

  1. Prostym przykładem

    Najpierw trzeba napisać gramatykę, który mówi instaparse jak analizować dany wyraz. Zaczniemy po prostu parsowania numer 1:

    (def parser 
        (insta/parser 
         "sexp = number 
         number = #'[0-9]+' 
         ")) 
    

    s-wyrażenie opisuje gramatykę najwyższego poziomu dla sexpression. Nasza gramatyka mówi, że sexp może mieć tylko liczbę. Następny wiersz oznacza, że ​​liczba może być dowolną cyfrą 0-9, a + jest podobna do regex +, co oznacza, że ​​musi ona mieć jedną liczbę powtórzoną dowolną liczbę razy. Jeżeli możemy uruchomić naszego parsera otrzymujemy następujące parsującej drzewa:

    (parser "1")  
    => [:sexp [:number "1"]] 
    

    Ingoring nawiasie

    Możemy ignorować pewne wartości dodając nawiasy kątowe < do naszej gramatyki. Więc jeśli chcemy analizować "(1)" jako po prostu 1 możemy wyprostować naszą gramatykę jak:

    (def parser 
        (insta/parser 
         "sexp = lparen number rparen 
         <lparen> = <'('> 
         <rparen> = <')'> 
         number = #'[0-9]+' 
         ")) 
    

    i jeżeli ponownie uruchamiamy parser będzie ignorować lewy i prawy nawias:

    (parser "(1)") 
    => [:sexp [:number "1"]] 
    

    Ta wola stać się pomocnym, kiedy piszemy gramatykę dla seksu poniżej.

    Dodawanie przeznaczone

    teraz się dzieje, jeśli dodamy przestrzenie i biegać (parser "(1)")? Cóż, otrzymujemy błąd:

    (parser "(1)") 
    => Parse error at line 1, column 2: 
        (1) 
        ^
        Expected: 
        #"[0-9]+" 
    

    To dlatego, że nie zdefiniowaliśmy pojęcia przestrzeni w naszej gramatyce!Tak więc możemy dodać przestrzenie, takie jak:

    (def parser 
        (insta/parser 
         "sexp = lparen space number space rparen 
         <lparen> = <'('> 
         <rparen> = <')'> 
         number = #'[0-9]+' 
         <space> = <#'[ ]*'> 
         ")) 
    

    Znowu * jest podobna do regex * a to oznacza zero lub więcej niż jedno wystąpienie pewnej przestrzeni. Oznacza to, że poniższe przykłady będą wszyscy zwracają ten sam wynik:

    (parser "(1)")   => [:sexp [:number "1"]] 
    (parser "(1)")  => [:sexp [:number "1"]] 
    (parser "(  1)") => [:sexp [:number "1"]] 
    
  2. Budowanie s-wyrażenie

    Jesteśmy powoli będziemy budować naszą gramatykę od podstaw. Przydatne może być spojrzenie na ostateczny produkt here, aby pokazać, dokąd zmierzamy.

    Tak więc sexp zawiera więcej niż liczby zdefiniowane przez naszą prostą gramatykę. Jednym z poglądów wysokiego poziomu, jakie możemy mieć z seksu, jest postrzeganie ich jako operacji między dwoma nawiasami. Zasadniczo jako (operation). Możemy napisać to bezpośrednio w naszej gramatyce.

    (def parser 
        (insta/parser 
         "sexp = lparen operation rparen 
         <lparen> = <'('> 
         <rparen> = <')'> 
         operation = ??? 
         ")) 
    

    Jak już wspomniano powyżej, wsporniki kątowe < powiedzieć instaparse ignorować te wartości, gdy jest co drzewo przetworzenia. Co to jest operacja? Cóż, operacja składa się z operatora, takiego jak +, oraz niektórych argumentów, takich jak numery 1 i 2. Można więc powiedzieć, napisać naszą gramatykę jak:

    (def parser 
        (insta/parser 
         "sexp = lparen operation rparen 
         <lparen> = <'('> 
         <rparen> = <')'> 
         operation = operator + args 
         operator = '+' 
         args = number 
         number = #'[0-9]+' 
         ")) 
    

    My stwierdzono tylko jedną możliwą operatora, +, tak aby zachować rzeczy proste. Zawarliśmy także regułę gramatyki liczbowej na podstawie prostego przykładu powyżej. Nasza gramatyka jest jednak bardzo ograniczona. Jedynym prawidłowym seksem, który można przeanalizować, jest (+1). To dlatego, że nie uwzględniliśmy pojęcia przestrzeni i stwierdziliśmy, że argumenty mogą mieć tylko jedną liczbę. W tym kroku zrobimy dwie rzeczy. Dodamy spacje i stwierdzimy, że argumenty mogą mieć więcej niż jeden numer.

    (def parser 
        (insta/parser 
         "sexp = lparen operation rparen 
         <lparen> = <'('> 
         <rparen> = <')'> 
         operation = operator + args 
         operator = '+' 
         args = snumber+ 
         <snumber> = space number 
         <space> = <#'[ ]*'> 
         number = #'[0-9]+' 
         ")) 
    

    Dodaliśmy space za pomocą reguły gramatyki przestrzeni zdefiniowanej w prostym przykładzie. Stworzyliśmy nowy snumber, który jest zdefiniowany jako space i number, i dodaje + do snumber, aby stwierdzić, że musi pojawić się raz, ale może powtórzyć dowolną liczbę razy. Tak więc możemy uruchomić nasz parser jak tak:

    (parser "(+ 1 2)") 
    => [:sexp [:operation [:operator "+"] [:args [:number "1"] [:number "2"]]]] 
    

    możemy uczynić nasze gramatyki bardziej wytrzymałe poprzez args odesłanie do sexp. W ten sposób możemy mieć sex w naszym sexp! Możemy to zrobić, tworząc ssexp, który dodaje space do sexp, a następnie dodaje ssexp do do .

    (def parser 
        (insta/parser 
         "sexp = lparen operation rparen 
         <lparen> = <'('> 
         <rparen> = <')'> 
         operation = operator + args 
         operator = '+' 
         args = snumber+ ssexp* 
         <ssexp> = space sexp 
         <snumber> = space number 
         <space> = <#'[ ]*'> 
         number = #'[0-9]+' 
         ")) 
    

    Teraz możemy uruchomić

    (parser "(+ 1 2 (+ 1 2))") 
    => [:sexp 
         [:operation 
         [:operator "+"] 
         [:args 
         [:number "1"] 
         [:number "2"] 
         [:sexp 
          [:operation [:operator "+"] [:args [:number "1"] [:number "2"]]]]]]] 
    
  3. Transformacje

    Ten krok można wykonać za pomocą dowolnej liczby narzędzi, które działają na drzewach, takich enlive, zamki błyskawiczne, mecz, a drzewo -seq. Instaparse zawiera jednak także swoją użyteczną funkcję o nazwie insta\transform. Możemy budować nasze transformacje, zastępując klucze w naszym drzewie parse przez prawidłowe funkcje clojure.Na przykład :number staje się read-string, aby zamienić nasze łańcuchy na poprawne liczby, :args staje się vector, aby budować nasze argumenty.

    Tak, chcemy przekształcić następująco:

    [:sexp [:operation [:operator "+"] [:args [:number "1"] [:number "2"]]]] 
    

    Do tego:

    (identity (apply + (vector (read-string "1") (read-string "2")))) 
    => 3 
    

    Możemy to zrobić poprzez zdefiniowanie nasze opcje Transformacja:

    (defn choose-op [op] 
    (case op 
        "+" +)) 
    (def transform-options 
        {:number read-string 
        :args vector 
        :operator choose-op 
        :operation apply 
        :sexp identity 
    }) 
    

    Jedynym trudne rzeczy tutaj dodano funkcję choose-op. Chcemy przekazać funkcję + do , ale jeśli zastąpimy operator przez +, użyjemy + jako zwykłej funkcji. Tak będzie przekształcać nasze drzewo do tego:

    ... (apply (+ (vector ... 
    

    Ale używając choose-op to minie + jako argument do apply jako takie:

    ... (apply + (vector ... 
    

Wnioski

My możemy teraz uruchomić naszego małego interpretera, łącząc parser i transformator:

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

(lisp "(+ 1 2)") 
    => 3 

(lisp "(+ 1 2(+ 3 4))") 
    => 10 

Ostateczny kod użyty w tym samouczku można znaleźć pod następującym numerem: here.

Mam nadzieję, że to krótkie wprowadzenie wystarczy, aby rozpocząć realizację własnych projektów. Możesz dodawać nowe linie przez zadeklarowanie gramatyki dla \n i możesz nawet zdecydować, aby nie ignorować spacji w drzewie analizowania, usuwając kątowe nawiasy <. To może być pomocne, ponieważ starasz się zachować wcięcie. Mam nadzieję, że to pomoże, jeśli nie tylko napisz komentarz!

+1

Link do posta na blogu Carina Meiera to [this] (http://gigasquidsoftware.com/blog/2013/05/01/growing-a-language-with-clojure-and-instaparse/) – ducky

+0

dzięki, zaktualizowałem połączyć. – pooya72

Powiązane problemy