我们如何使用instaparse为clojure代码定义语法?

时间:2013-08-12 12:39:26

标签: clojure instaparse

我是解析的新手,希望分析一些clojure代码。我希望有人可以提供一个如何使用instaparse解析clojure代码的示例。我只需要做数字,符号,关键字,性别,向量和空格。

我想解析的一些例子:

(+ 1 2 
   (+ 3 4))

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

1 个答案:

答案 0 :(得分:25)

你的问题有两部分。第一部分是解析表达式

(+ 1 2 
   (+ 3 4))

第二部分是将输出转换为您想要的结果。为了更好地理解这些原则,我强烈推荐Udacity的Programming Languages course. Carin Meier blog post也很有帮助。

理解解析器如何工作的最佳方法是将其分解为更小的部分。所以在第一部分我们只研究一些解析规则,在第二部分我们将构建我们的性别。

  1. 一个简单的例子

    首先需要编写一个语法来告诉instaparse如何解析给定的表达式。我们首先解析数字1

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

    sexp描述了性别压抑的最高级别语法。我们的语法表明,sexp只能有一个数字。下一行指出该数字可以是任何数字0-9,+类似于正则表达式+,这意味着它必须重复任意次数的一个数字。如果我们运行我们的解析器,我们得到以下解析树:

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

    输入括号

    我们可以通过在我们的语法中添加有角度的括号<来忽略某些值。因此,如果我们想简单地将"(1)"解析为1,我们可以将语法改为:

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

    如果我们再次运行解析器,它将忽略左右括号:

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

    当我们在下面写下sexp的语法时,这将会有所帮助。

    添加空格

    如果我们添加空格并运行(parser "( 1 )"),现在会发生?好吧,我们得到一个错误:

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

    那是因为我们没有在语法中定义空间的概念!所以我们可以添加空格:

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

    *再次类似于正则表达式*,它意味着零或多于一个空格。这意味着以下示例将返回相同的结果:

    (parser "(1)")         => [:sexp [:number "1"]]
    (parser "( 1 )")       => [:sexp [:number "1"]]
    (parser "(       1 )") => [:sexp [:number "1"]]
    
  2. 构建Sexp

    我们正在慢慢地从头开始构建我们的语法。查看最终产品here可能会有用,只是为了概述我们的目标。

    因此,一个sexp包含的不仅仅是我们简单语法定义的数字。我们可以对sexp进行的一个高级视图是将它们视为两个括号之间的操作。所以基本上是( operation )。我们可以直接将它写入我们的语法。

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

    如上所述,有角度的括号<告诉instaparse在制作解析树时忽略这些值。现在什么是操作?一个操作包含一个运算符,如+,以及一些参数,如数字12。所以我们可以把我们的语法写成:

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

    我们只说了一个可能的运算符+,只是为了简单起见。我们还从上面的简单示例中包含了数字语法规则。然而,我们的语法非常有限。它可以解析的唯一有效性别是(+1)。那是因为我们还没有包含空格的概念,并且声明args只能有一个数字。所以在这一步中我们将做两件事。我们将添加空格,我们将声明args可以有多个数字。

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

    我们使用我们在简单示例中定义的空间语法规则添加了space。我们创建了一个新的snumber,其定义为spacenumber,并将+添加到snumber中以声明它必须出现一次,但它可以重复任意数量的倍。所以我们可以这样运行我们的解析器:

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

    我们可以通过将args引用回sexp来使我们的语法更加健壮。这样我们就可以在我们的sexp中拥有sexp!我们可以通过创建ssexpspace添加sexp,然后将ssexp添加到args来实现此目的。

    (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]+'
            "))
    

    现在我们可以运行

    (parser "(+ 1 2 (+ 1 2))")
     =>   [:sexp
           [:operation
            [:operator "+"]
            [:args
             [:number "1"]
             [:number "2"]
             [:sexp
              [:operation [:operator "+"] [:args [:number "1"] [:number "2"]]]]]]]
    
  3. <强>转换

    此步骤可以使用任意数量的工具在树上完成,例如活动,拉链,匹配和树序列。然而,Instaparse还包括它自己的有用函数insta\transform。我们可以通过有效的clojure函数替换我们的解析树中的键来构建我们的转换。例如,:number变为read-string以将字符串转换为有效数字,:args变为vector以构建我们的参数。

    所以,我们想要改变这个:

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

    进入这个:

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

    我们可以通过定义转换选项来实现这一目标:

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

    这里唯一棘手的事情是添加函数choose-op。我们想要的是将函数+传递给apply,但如果我们将operator替换为+,它将使用+作为常规函数。所以它会把我们的树变成这样:

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

    但是,使用choose-op它会将+作为参数传递给apply,如下所示:

     ... (apply + (vector ...
    
  4. <强>结论

    我们现在可以通过将解析器和变换器放在一起来运行我们的小解释器:

    (defn lisp [input]
       (->> (parser input) (insta/transform transform-options)))
    
    (lisp "(+ 1 2)")
       => 3
    
    (lisp "(+ 1 2(+ 3 4))")
       => 10
    

    您可以找到本教程中使用的最终代码here

    希望这个简短的介绍足以让你自己的项目。您可以通过声明\n的语法来添加新行,您甚至可以通过删除有角度的括号<来选择不忽略解析树中的空格。鉴于您正在尝试保留缩进,这可能会有所帮助。希望这有帮助,如果不只是写评论!