Elixir中如何使用 Leex 和 Yecc - 译

Elixir中如何使用 leex 和 yecc - 1【译】

翻译: horsley 2019-5-31

前言

最近在学习 Elixir,想把以前用 ruby 写的一个 DSL 迁移过来,因此看到了这篇文章,随手翻译,以兹备忘。 对于一些专业词汇,保留英文更有利于理解,因此不作翻译,加入自己的理解,记录如下:

  • grammar - 文法:应该比词法、语法概念更大,是两者的综合
  • parser - 语法分析器:比如 1 + 2 就是一个语法
  • lexer - 词法分析器:1 + 2 会分解成 num, :+ , num 三个 token
  • token - (词法分析后得到的)关键词
  • ast - 抽象语法树
  • definition - .xrl 文件中:定义匹配词的正则
  • rule - .xrl 中定义词到 token 的转换规则

原文地址


如果你需要解析些什么东西的话,会发现 Leex 和 Yecc 是难以置信的强大。不幸的是你需要下点功夫去理解它。如果你像我一样是个 elixir 开发者的话,你会发现他们描述 tokens 和 grammars 的 DSL没有那么一目了然。我花了几天的功夫来研究这些工具,因为我并不会经常编写解析器,所以把我的一些心得记录下来,以备后用。这是两篇文章的第一篇。本文中,我们会使用 leex 和 yecc 来解析一个简单 grammar ,并且解释他们是如何一起协同工作的。下一篇文章,我会探讨如何使用他们来创建一个更为复杂的 grammar 的解析器(parser),以及我学到的一些单元测试技巧等等。凭良心说,对于这些工具我不是什么专家,我只是第一次使用并努力掌控他们的家伙,并最终试图理解他们的工作机理。

如果你喜欢跟着代码来,可以访问这里: 示例代码

起步:先让这些工具跑起来

Mix 对这些处理的很好。你需要做的仅仅是在 project 的根目录下创建 src 目录。然后将 .xrl 和 .yrl 文件放进去,mix 就会注意他们,依次识别出他们是 leex 和 yecc 文件,然后将他们编译成 .erl 文件,然后再编译这些 erl 文件,所有这些都是自动的,非常简单。

第一步:理解这些工具是干嘛的

leex 是一个 lexer(词法分析器)。它读取输入数据,根据你定义的 rule 识别 tokens,将这些 tokens 转换成你想要的东西,以列表的形式保存。列表中可以保存任意元素,在本文中,我们随后需要使用 yecc,因此我们最好将这些 tokens 转换成 yecc 喜欢的样式,下面是我们要干的第一件事。

我写了一个简单 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。

我们看下 lexer 是如何构建的

%% 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 文件由三部分构成,这三部分都是必须的。

第一部分是 Definitions。

这一部分我们会定义一些模板, lexer 以此来扫描输入数据,进行适配识别。每一行的格式都是左边匹配右边的正则表达式,如 Whitespace = [\s\t]。注意,这些都是 Erlang 语言的正则表达,它是传统正则表达式的子集,因此遇到复杂的表达式,你需要更有创造力些,自己想想办法。 细节可以参考 leex 文档。

要引用一条 definition,将其放到{}花括号里面就行了,Definitions 部分或者 Rules 部分都可以这样引用。

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 语法解析器

在我们开始学习 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 文件由四部分构成:

  • Nonterminals
  • Terminals
  • Rootsymbol
  • Erlang code.

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 是非常有帮助的。我建议你也去看看。

转载于:https://my.oschina.net/u/4130622/blog/3056673

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值