9

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

我希望解析的一些示例:

(+ 1 2 
   (+ 3 4))

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

1 回答 1

26

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

(+ 1 2 
   (+ 3 4))

第二部分是将输出转换为您想要的结果。为了更好地理解这些原则,我强烈推荐 Udacity 的编程语言课程。Carin Meier 的博客文章也很有帮助。

了解解析器如何工作的最佳方法是将其分解为更小的部分。所以在第一部分我们将检查一些解析规则,在第二部分我们将构建我们的sexp。

  1. 一个简单的例子

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

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

    sexp 描述了 s 表达式的最高级别语法。我们的语法规定,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 不仅仅包含我们简单语法定义的数字。我们可以对 sexp 拥有的一种高级视图是将它们视为两个括号之间的操作。所以基本上作为一个( operation ). 我们可以直接把它写到我们的语法中。

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

    如上所述,尖括号<告诉 instaparse 在生成解析树时忽略这些值。现在什么是手术?一个操作由一个运算符(如+)和一些参数(如数字1和)组成2。所以我们可以说把我们的语法写成:

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

    为了简单起见,我们只声明了一种可能的运算符 ,+。我们还包含了上面简单示例中的数字语法规则。然而,我们的语法非常有限。它可以解析的唯一有效的 sexp 是(+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的定义为space和 a number,并添加+到 snumber 以声明它必须出现一次但它可以重复任意次数。所以我们可以这样运行我们的解析器:

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

    我们可以通过args引用 back 来使我们的语法更加健壮sexp。这样我们就可以在我们的性爱中拥有性爱!我们可以通过创建ssexpwhich 添加spacesexp然后添加到来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. 转型

    这一步可以使用任意数量的在树上工作的工具来完成,例如 enlive、zippers、match 和 tree-seq。然而,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 ...
    

结论

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

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

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

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

您可以在此处找到本教程中使用的最终代码。

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

于 2013-08-13T14:01:26.427 回答