最近在学习 Elixir,想把之前用 ruby 写的一个 DSL 迁移过来,所以看到了这篇文章,随手翻译,以兹备忘。 对于一些专业词汇,保留英文更有利于理解,所以不做翻译,加入本身的理解,记录以下:html
原文地址git
若是你须要解析些什么东西的话,会发现 Leex 和 Yecc 是难以置信的强大。不幸的是你须要下点功夫去理解它。若是你像我同样是个 elixir 开发者的话,你会发现他们描述 tokens 和 grammars 的 DSL没有那么一目了然。我花了几天的功夫来研究这些工具,由于我并不会常常编写解析器,因此把个人一些心得记录下来,以备后用。这是两篇文章的第一篇。本文中,咱们会使用 leex 和 yecc 来解析一个简单 grammar ,而且解释他们是如何一块儿协同工做的。下一篇文章,我会探讨如何使用他们来建立一个更为复杂的 grammar 的解析器(parser),以及我学到的一些单元测试技巧等等。凭良心说,对于这些工具我不是什么专家,我只是第一次使用并努力掌控他们的家伙,并最终试图理解他们的工做机理。github
若是你喜欢跟着代码来,能够访问这里: 示例代码正则表达式
Mix 对这些处理的很好。你须要作的仅仅是在 project 的根目录下建立 src 目录。而后将 .xrl 和 .yrl 文件放进去,mix 就会注意他们,依次识别出他们是 leex 和 yecc 文件,而后将他们编译成 .erl 文件,而后再编译这些 erl 文件,全部这些都是自动的,很是简单。ruby
leex 是一个 lexer(词法分析器)。它读取输入数据,根据你定义的 rule 识别 tokens,将这些 tokens 转换成你想要的东西,以列表的形式保存。列表中能够保存任意元素,在本文中,咱们随后须要使用 yecc,所以咱们最好将这些 tokens 转换成 yecc 喜欢的样式,下面是咱们要干的第一件事。app
我写了一个简单 lexer。这个 lexer 的惟一功能就是识别整数和浮点数,而且忽略空白字符和逗号。在咱们研究代码前,先展现下他们的实际功能:函数
iex(1)> :number_lexer.string('12 42 23.24 23') {:ok, [{:int, 1, 12}, {:int, 1, 42}, {:float, 1, 23.24}, {:int, 1, 23}], 1} iex(2)>
这个 lexer 读入一个 char list,返回一个 tuple {:ok,list_of_tokens,line}。list_of_tokens 中的每个 token 又是以下格式 {type,line_number,value}。这种格式是 yecc 将要用到的。工具
咱们从 lexer 获得输出,传入 parser,而后 parser 运用语法规则产生 AST。单元测试
%% src/number_lexer.xrl Definitions. Whitespace = [\s\t] Terminator = \n|\r\n|\r Comma = , Digit = [0-9] NonZeroDigit = [1-9] NegativeSign = [\-] Sign = [\+\-] FractionalPart = \.{Digit}+ IntegerPart = {NegativeSign}?0|{NegativeSign}?{NonZeroDigit}{Digit}* IntValue = {IntegerPart} FloatValue = {IntegerPart}{FractionalPart}|{IntegerPart}{ExponentPart}|{IntegerPart}{FractionalPart}{ExponentPart} Rules. {Comma} : skip_token. {Whitespace} : skip_token. {Terminator} : skip_token. {IntValue} : {token, {int, TokenLine, list_to_integer(TokenChars)}}. {FloatValue} : {token, {float, TokenLine, list_to_float(TokenChars)}}. Erlang code.
.xrl 文件由三部分构成,这三部分都是必须的。学习
这一部分咱们会定义一些模板, lexer 以此来扫描输入数据,进行适配识别。每一行的格式都是左边匹配右边的正则表达式,如 Whitespace = [\s\t]。注意,这些都是 Erlang 语言的正则表达,它是传统正则表达式的子集,所以遇到复杂的表达式,你须要更有创造力些,本身想一想办法。 细节能够参考 leex 文档。
要引用一条 definition,将其放到{}花括号里面就好了,Definitions 部分或者 Rules 部分均可以这样引用。
用来真正识别 token,并告诉 leex 如何处理。rules 格式必须以下:
<pattern> : <result>.
冒号先后必须留空格,末尾的点必须。
在实际的 Erlang 代码中,咱们还要作些灵活处理,用来生成咱们须要的东西。好比这行代码用来建立浮点数 token:
{FloatValue} : {token, {float, TokenLine, list_to_float(TokenChars)}}.
代码含义是,当你匹配到上面的 FloatValue 模板时,生成一个 token,格式为 {<atom>, <line#>, <value>}。这里的 atom 在 Elixir 语言中就是 :float,line# 行号由 leex 生成,value 由 Erlang 语言的内建函数 list_to_float 生成。
对于相似 Whitespace 和 Comma 这样的模板,咱们经过设置为 skip_token 将匹配结果直接丢弃。
在咱们开始学习 parser 前,先让咱们设计一下值得解析的东西。咱们但愿咱们的微型语言具有表达 lists 的能力,lists 用来存放 ints 和 floats,用逗号分隔,列表可嵌套,好比 [1,2,3,[2.1,2,2]]。首先咱们须要修改 lexer,须要识别带方括号的 tokens。在 Definitions 部分添加以下一行:
Bracket = [\[\]]
这一句匹配任意方括号,而后咱们将 bracket(转换成 atom,这很是重要,由于 parser 只能识别 atom)传递给 parser:
{Bracket} : {token, {list_to_atom(TokenChars), TokenLine, TokenChars} }.
如今咱们有一点解析的工做要作了,让咱们将关注点移到 yecc。第一次咱们的微型语言终于有点像个 grammar 了:
Document :: Value(list) Value :: Int Float List List :: Value(list)
咱们定义了 Document,其包含一个或多个 Values,而每个 value 能够是 Int,Float 或者一个包含更多 Values的 List。
如今让咱们看看 .yrl 文件的结构,学习下如何定义 parser。
yrl 文件由四部分构成:
terminals 是 grammar 的最小单元结构。terminals 是没法再进一步展开的。他们就是 leex 生成的 tokens,但这里咱们必须再次把他们罗列出来:
Terminals int float '[' ']'.
nonterminals 是经过 rules 构建的更为复杂的结构。很明显 document,value,list 都是这种结构。这里咱们还要添加一些辅助的 nonterminals,用来更好地实现 grammar。
Rootsymbol 表明 AST 的最顶层。这会是 parser 应用的第一条 rule,而后顺藤摸瓜,一步一步导入到更底层的 rules。
下面是一条 Rootsymbol 语句,咱们拥有了全部的 rules,每一条 rule 的格式以下:
<nonterminal> -> <pattern> : <result
其基本含义就是,“运用 pattern 进行匹配,若是成功了,返回解析对应的 nonterminal”。听起来有点绕吧,让咱们看看基于咱们的微型 grammar,它是如何运做的:
%% src/number_parser.yrl Nonterminals document values value list list_items. Terminals int float '[' ']'. Rootsymbol document. document -> values : '$1'. values -> value : ['$1']. values -> value values : ['$1'] ++ '$2'. value -> int : {int, unwrap('$1')}. value -> float : {float, unwrap('$1')}. value -> list : '$1'. list -> '[' list_items ']' : '$2'. list_items -> value : ['$1']. list_items -> value list_items: ['$1'] ++ '$2'. Erlang code. unwrap({_,_,V}) -> V.
咱们设定 document 做为 Rootsymbol,所以它是 grammar 的最顶层,而后咱们设定 int,float,方括号做为 terminals,这些东西对应 lexer 中产生的 tokens。而后咱们设定 rules,可能语法会比较怪。咱们看下 value 的 rules。
value -> int : {int, unwrap('$1')}. value -> float : {float, unwrap('$1')}. value -> list : '$1'.
第一行是说若是你有个 int token,而后咱们就能构造一个 value。代码中冒号后面的代码就是构造器,咱们进一步分解。'$1'表示模板匹配中的第一个元素。这种场景下,pattern(int) 中只有一个元素,就是它自己所表明的值。这个 int 是 lexer 产生的 token,咱们回想下,lexer 产生的 tokens 是这种格式 {type_atom, line_num, value}。此时咱们知道它是一个 int,咱们不须要行号,咱们关心的只是它的值。咱们编写了一个 helper 方法 unwrap,恰好放到文件的最底部,也就是 Erlang code 部分。这种状况下,它会返回一个包含整数表现形式的 char_list,而后咱们会返回一个包含 int atom 的 tuple,还包含 char list。这里咱们将 char list 转换成 integer,而后简单的将其返回。这就是一种设计方式,就看你怎么处理了。
接下来看 float 的 rule,它的工做方式同 int是同样的,代码自己一目了然。
最后一个是 list 的 rule,此时,你会注意到咱们没有对 list 的值作 unwrap,咱们只是直接返回值,由于它不是由 lexer 建立的,它是由咱们的其余规则建立的。 具体以下:
list -> '[' list_items ']' : '$2'. list_items -> value : ['$1']. list_items -> value list_items: ['$1'] ++ '$2'
这个东西看上去有点复杂。第一条 rule 就是说:所谓 list 就是一对方括号里面放上 list_items,其值由第二部分的 pattern 提供,就是 list_items。这条规则用来强制保证方括号的存在。
下一条 rule 用来描述一个 list_items 只包含一个元素的状况。它会接受一个单一值,在列表中返回 ([$1]),其后第二条 rule 递归地获取值,放入 list 中,而后追加到已存在的 list_items中。这种方式在 yecc 中是很是通用的捕获列表的方式,咱们编写一样的模板,让 document 可以容纳列表值,代码以下:
values -> value : ['$1']. values -> value values : ['$1'] ++ '$2'.
让咱们将 lexer 跟 parser 放到一块儿:
iex(1)> {:ok, tokens, _} = :number_lexer.string('1,[2,3]') {:ok, [{:int, 1, 1}, {:"[", 1, '['}, {:int, 1, 2}, {:int, 1, 3}, {:"]", 1, ']'}], 1} iex(2)> :number_parser.parse(tokens) {:ok, [{:int, 1}, [int: 2, int: 3]]}
正常工做了!下一篇文章,我会讨论一些小技巧,用于表示更复杂的 grammar。 在本文写做期间,我发现 Knut Nesheim’s simple calculator app calx 和这篇文章 on the relops blog 是很是有帮助的。我建议你也去看看。