Context-free Grammar, Part II Botton-Up Parsing

Table of Contents

1 自底向上分析法(Bottom-up)

An LR parser scans and parses the input text in one forward pass over the text. The parser builds up the parse tree incrementally, bottom up, and left to right, without guessing or backtracking.

Sorry, your browser does not support SVG.

Figure 1: Bottom-up parse of A*2 + 1

参考:https://en.wikipedia.org/wiki/LR_parser

1.1 可行前缀和句柄

本节内容详细请参考:编译原理及实践(冯博琴译),5.1节。

在介绍可行前缀之前先介绍一下什么是最右句型(right sentential form)。
我们知道,推导可分为最左推导(每一步推导时替换最左边的非终结符)和最右推导(每一步推导时替换最右边的非终结符),自顶向下分析对应着输入串的最左推导过程,自底向上分析对应着输入串的最右推导过程。

最右句型是指最右推导过程中的每一个中间串。
例如,有文法

E' -> E
E -> E + n | n

2 展示了串n + n使用这个文法的自底向上分析过程。

cfg_bottom_up_example.png

Figure 2: 串n + n使用上面文法的自底向上分析过程

在图 2 中的“动作”那列中,由下往上可以得到其对应的推导(最右推导)是: E' => E => E + n => n + n
在分析过程中,最右句型被分析栈和输入分隔开。

分析栈中的符号序列称为最右句型的可行前缀。 上面例子中,E、E +和E + n都是最右句型E + n的可行前缀;ε和n是最右句型n + n的可行前缀,注意n +不是n + n的可行前缀。

分析过程中, 发生归约动作时使用的产生式称为句柄(一般仅指产生式的右部分),句柄在被识别之前总是位于分析栈的顶部。 上面例子中,句柄有:n,E + n,E,它们都是归约动作中使用的产生式,且出现在分析栈的顶部。

由于“移进”和“归约”是自底向上分析过程中的两个主要动作,所以“自底向上分析程序”有时也称为“移进——归约分析程序”。 识别句柄是“移进——归约分析程序”的主要任务。

1.2 LR(0)分析

1.2.1 利用自动机进行自底向上语法分析

下面将以LR(0)分析为例描述有限自动机和语法分析的关系。详细请参考:编译原理及实践(冯博琴译),5.2.1节。
基本流程为: 产生式–—>LR(0)项–—>NFA(LR(0)项作为NFA有限自动机的状态,这个有限自动机保存着移进——归约分析的过程)–—>DFA
DFA构造好后,就可以用它来识别某个具体的输入串了。

cfg_LR0_ex1_1.png

Figure 3: 产生式(扩充文法)

cfg_LR0_ex1_2.png

Figure 4: 对应的LR(0)项

cfg_LR0_ex1_3.png

Figure 5: 对应的NFA

cfg_LR0_ex1_4.png

Figure 6: 对应的DFA

注:当然yacc等工具不用中间生成NFA,可以直接从产生式得到DFA。

1.2.2 什么是LR(0)项

LR(0)项就是在其右边带有“位置信息”的产生式选择。 一般用一个点号来表示这个位置信息。

cfg_LR0_item.png

Figure 7: 摘自《编译原理(第2版)》4.6.2节

LR(0)项可以作为一个保存有分析栈和移进——归约分析过程的信息的有穷自动机状态来使用。

1.2.3 由LR(0)项构造NFA(定义状态转换规则)

要构造LR(0)项对应的NFA,首先得定义自动机状态间的转换规则。详细请参考:编译原理及实践(冯博琴译),5.2.2节。

cfg_LR0_to_NFA.jpg

Figure 8: 由LR(0)项构造NFA

从上面分析知,由LR(0)项构造NFA包含两种情况。第一种情况是显而易见的,第二种情况说明了 如何添加“ε-转换”,即对每个A->α.Xβ,添加ε-转换到所有可能的X->.β

如,下面有8个LR(0)项,它们对应的NFA是什么呢?

cfg_LR0_ex1_2.png

Figure 9: 对应的LR(0)项

8个LR(0)项对应着NFA的8个状态,由前面介绍的构造LR(0)项对应NFA的规则可以得到其NFA为(下图中, ε-转换是这样添加的:对每个A->α.Xβ,添加ε-转换到所有可能的X->.β ):

cfg_LR0_ex1_3.png

Figure 10: 对应的NFA

有了NFA,就可以通过子集构造法得到DFA(如图 6 所示)了,有了DFA,再加上LR(0)分析算法指定的动作,就可以得到文法的分析表了。

1.2.4 LR(0)项中内核项和非内核项(又称为闭包项)

在介绍内核项和非内核项(闭包项)前,先介绍项集的闭包。以LR(0)项为例。

cfg_item_closure.png

Figure 11: 项集的闭包

由上面的定义知,如果某个点在最左端的B产生式被加入到I的闭包中,则所有点在最左端的B产生式都会被加入到这个闭包中。由于有这个性质,我们可以少关注那些可以通过闭包运算得到的项(闭包项)。
我们把项分为下面两类:
(1) 内核项:包括初始产生式 S' -> .S 和点不在最左端的所有项。
(2) 非内核项(闭包项):除了 S' -> .S 之外的点在最左端的所有项。
区分内核项和非内核项(闭包项)的重要意义在于:若有一个文法,内核项唯一地判断出状态以及它的转移,那么只需要指出内核项就可以完整地表示出项目集合的DFA来。
LR(0)项的闭包实际上体现的就是LR(0)项的自动机中转换规则定义中的第二情况(如何添加“ε-转换”)。

cfg_kernal_closure.jpg

Figure 12: DFA中阴影部分的项是非内核项(摘自《编译原理(第2版)》4.6.2节)

显然,所有非内核项(闭包项)都会出现在项集 \(I_0\) 中。

1.2.5 LR(0)分析算法

详细请参考:编译原理及实践(冯博琴译),5.2.3节。

LR(0)分析算法很简单,描述如下:
令s为当前state(位于分析栈顶部),则动作定义为:
1.如果s中包含了形式为A->α.Xβ的项(称为“移入”项),其中X是一个终结符。则动作是将当前的输入记号移入栈中,若输入记号为X,则压入栈中的新state为包含了A->αX.β的state;若输入记号不为X,则报错。
2.如果s中包含了形式为A->γ.的项(称为“完整”项)。则动作是用产生式A->γ归约,分析栈按下面规则更新:首先把γ和相应的state从分析栈中删除,这样分析栈回到了构造γ前的state(这个state肯定含有一个形式为B->α.Aβ的项),把A压入到分析栈,再压入那个包含项B->αA.β的state。

若某文法用上面的分析算法没有歧义,则称它为LR(0)文法。 显然很少有实用的文法为LR(0)文法。

1.2.6 LR(0)文法的等价描述

一个文法是LR(0)文法当且仅当它的DFA中每个state要么仅包含一个或多个“移入”项,要么仅包含单个“完整”项 (显然,按照LR(0)算法,若某个state中既含有“移入”项又含有“完整”项则会出现shift/reduce冲突,若某个state中含有多个“完整”项则会出现reduce/reduce冲突)。

从上可知,LR(0)对文法的限制是很严格的。

1.2.7 LR(0)文法实例

考虑文法:A -> (A) | a
它对应的LR(0)项的DFA如下图所示,这是一个LR(0)文法,因为state 0, state 3, state 4仅包含“移入”项,而state 1, state 2, state 5都是单个“完整”项。

cfg_LR0_ex2_DFA.png

Figure 13: 文法“A -> (A) | a”对应的DFA

下图详细说明了如何用上面的DFA来分析串((a))的过程。

cfg_LR0_ex2_process.png

Figure 14: 分析串((a))的过程

DFA常用文法分析表来表示,文法“A -> (A) | a”对应的分析表:

cfg_LR0_ex2_parsing_table.png

Figure 15: 文法“A -> (A) | a”对应的分析表(空白的地方表示出错)

1.2.8 另解LR(0)自动机

LR(0)自动机也可用可行前缀和句柄来表达。
The LR(0) automaton is a DFA which accepts viable prefixes of right sentential forms, ending in a handle.

参考:http://www.cs.uaf.edu/~cs631/notes/parse/node5.html

1.3 SLR(1)分析

1.3.1 构造SLR(1)对应的自动机

SLR(1)分析使用的自动机就是LR(0)项对应的自动机。
它通过使用输入串中的下一个记号来指导它的动作,使得分析能力大大提高。

1.3.2 SLR(1)分析算法

SLR(1)分析是在LR(0)上的一个简单有效的扩展(SLR(1)通过向前看符号的信息使得其分析能力得到增强),这个扩展使得SLR(1)能处理很多实际的语言结构。

SLR(1)分析算法如下所示:

cfg_SLR1.png

Figure 16: The SLR(1) parsing algorithm

如果用上面的算法分析文法没有歧义产生,则该文法称为SLR(1)文法。

参考:编译原理及实践(冯博琴译),5.3.1节。

1.3.3 SLR(1)文法的等价描述

SLR(1)文法的定义不是很直观。下面描述一个等价的直观描述。

当且仅当任何状态s满足下面两个条件时,文法就为SLR(1)文法:
条件1、对于在s中的任何项A->α.Xβ,其中X是终结符,是向前看符号,则s中可以包含“完整”项,但不能包含这样的“完整”项B->γ.,其中X在Follow(B)中。
条件2、对于在s中的任何两个“完整”项A->α.和A->β.,Follow(A) ∩ Follow(B)为空。

说明1:条件1中,如果包含了“完整”项B->γ.,其中X在Follow(B),就会出现shift-reduce冲突(A->α.Xβ可以shift,而B->γ.可以reduce);条件2中,如果两个“完整”项A->α.和A->β.,Follow(A) ∩ Follow(B)不为空,就会出现reduce-reduce冲突。
说明2:前面提到,LR(0)文法的限制的重要特点是:DFA的每个state只有两种可能性:1.全部为“移入”项;2.仅有1个“完整”项。 SLR(1)文法对LR(0)文法的两个限制都进行了放宽。

1.3.4 SLR(1)分析实例

以文法:S -> (S)S | ε为例,它对应的SLR(1)分析表如下:

cfg_SLR1_ex1_parsing_tab.png

Figure 17: 文法“S -> (S)S | ε”的分析表,accept表示 r(S'->S)

用它来分析输入串()()的过程如下:

cfg_SLR1_ex1_process.png

Figure 18: 利用上面文法分析输入串()()的过程

1.3.5 SLR(1)不够强大

《编译原理(第2版)》4.6.4节例4.48的文法不是SLR(1),例4.61表明这个文法属于LALR(1)。

1.4 LR(1)分析(即Canonical LR分析)

在SLR(1)算法中,“先行符号”已经被利用来指导动作。
LR(1)的思想是除了用“先行符号”来指导动作外,再进一步利用“先行符号”,把“先行符号”的信息构建在DFA中!
这个新的DFA使用的是LR(1)项,而不是LR(0)项。LR(1)项是由LR(0)项和一个先行记号组成。可以用中括号将LR(1)项写为下面形式:
\[[A \to \alpha . \beta , \, a]\]
其中,A->α.β是LR(0)项,而a是一个先行记号,LR(0)项和先行记号之间用逗号分开。

1.4.1 构造LR(1)项对应的自动机

首先,定义LR(1)项之间的转换。详细请参考:编译原理及实践(冯博琴译),5.4.1节。

cfg_LR1_transition.jpg

Figure 19: LR(1)项之间的转换

定义由两部分组成。第一部分定义中,转换状态前后的先行符号是相同的;第二部分定义“创建”了新的先行。
第一部分定义很简单,它和LR(0)项转换规则的第一部分定义基本类似:
\[[A\to\alpha.X\gamma,\,a]\overset{X}{\longrightarrow}[A\to\alpha X.\gamma,\,a]\]
第二部分定义也不难理解。它比LR(0)项转换规则第二部分定义更加精细!就是这点精细使得LR(1)分析时产生冲突的机会变小,分析能力大大提高。
下面来说明第二部分定义的由来。显然,B为非终结符时,有下面“ε-转换”:
\[[A\to\alpha.B\gamma,\,a]\overset{\epsilon}{\longrightarrow}[B\to . \beta,\,lookahead]\]
如果不特别限制lookahead,则lookahead显然是Follow(B)中的元素。其实我们可以合理地缩小lookahead的范围(这样分析时产生冲突的机会会变小),把lookahead限制在First(γa)中,即:
\[[A\to\alpha.B\gamma,\,a]\overset{\epsilon}{\longrightarrow}[B\to . \beta,\,b]\quad b\in First(\gamma a)\]
这样做是合理的, 项 \([A\to\alpha.B\gamma,\,a]\) 说明了在分析的这一点上可能要识别B,再识别γ,最后移入a。所是当识别B时可以限制lookahead在First(γa)中。

cfg_LR1_transition_2.jpg

Figure 20: 识别非终结符B时可以限制lookahead在First(γa)中

1.4.2 LR(1)项中的内核项和非内核项

对于Gnu bison,下面第一个命令仅会报告内核项,而第二个命令会报告内核项和非内核项(闭包项):

$ bison --report=state calc.y          # 仅会报告内核项
$ bison --report=itemset calc.y        # 报告内核项和非内核项(闭包项)

注:yacc默认只报告内核项。

1.4.3 LR(1)项集族的构造(如何产生LR(1)的DFA)

cfg_LR1_items.jpg

Figure 21: LR(1)项集族的构造

参考:《编译原理(第2版)》4.7.2节。

1.4.4 实例说明“LR(1)项的NFA和DFA”

下面例子摘自:Parsing Techniques - A Practical Guide,2nd, 9.6.1节。

假设有下面文法:

S -> ABc
A -> a
B -> b
B -> ε

上面LR(1)文法其对应的NFA为(图中LR(1)项省略了中括号,且是用空格而不是逗号分开LR(0)项和先行符号,用#而不是$表示结束符):

cfg_LR1_ex1_NFA.jpg

Figure 22: LR(1)项对应的NFA

转换为DFA为:

cfg_LR1_ex1_DFA.png

Figure 23: LR(1)项对应的DFA

1.4.5 LR(1)分析算法

LR(1)分析算法和SLR(1)分析算法很相似。

cfg_LR1_parsing.png

Figure 24: LR(1)分析算法

参考:编译原理及实践(冯博琴译),5.4.2节。

1.4.6 LR(1)文法的等价描述

LR(1)文法的直观且等价的描述:
当且仅当任何状态s满足下面两个条件时,文法就为LR(1)文法:
条件1、对于在s中的任何项[A->α.Xβ , a],其中X是终结符,则s中不能有格式为[B->γ. , X]的项(否则就有一个shift/reduce冲突)。
条件2、在s中没有同时出现格式[A->α. , a]和[B->β. , a]的两个项(否则就有一个reduce/reduce冲突)。

说明1:LR(1)文法的特点和SLR(1)文法的特点比较类似。
说明2:LR(1)文法比SLR(1)文法更强大,具体强大在哪里可参见《编译原理(第2版)》4.7.1节。

1.4.7 LR(1)分析表

和前面描述LR(0)文法相同,我们仍然考虑文法:A -> (A) | a。
它对应LR(1)项的DFA为:

cfg_LR1_ex2_DFA.png

Figure 25: 文法“A -> (A) | a”对应LR(1)项的DFA

把这个DFA转换为LR(1)分析表,如下表所示:

cfg_LR1_ex2_parsing_tab.png

Figure 26: 文法“A -> (A) | a”对应LR(1)分析表

说明1:accept代表 reduce(A'->A)
说明2:用编号1和2分别表示产生式A->(A)和A->a,即:r1代表 reduce(A->(A)) ,r2代表 reduce(A->a)
说明3:空白的地方表示出错。

用LR(1)分析表来分析特定字符串的步骤和用SLR(1)分析表(或LR(0)分析表)的分析步骤是相同的。也就是说LR(1)算法的难点在于LR(1)分析表的构造!

1.5 LALR(1)分析

参考:编译原理及实践(冯博琴译),5.4.3节。

LALR(1)的功能比SLR(1)强,比LR(1)弱,但LALR(1)同时具有SLR(1)状态数量少的优点及LR(1)适用范围广的优点。

1.5.1 LALR(1)的基础——LR(1)项的DFA的两个特点

定义LR(1)项的DFA中的状态的核心是由LR(1)项的第1个成分组成的LR(0)项的集合(也就是说核心不包含向前看符号)。
由于LR(1)项的DFA的构造方法和LR(0)项的DFA构造方法有很多类似的地方,我们可以得到 LR(1)项的DFA有两个基本特点:
(1) LR(1)项的DFA的状态的核心是LR(0)项的DFA的一个状态;
(2) 若有具有相同核心的LR(1)项的DFA的两个状态s1和s2,假设在符号X上有一个从s1到状态t1的转换,则在X上一定还有一个从状态s2到状态t2的转换,且状态t1和t2具有相同的核心。
这两个基本特点是LALR(1)算法的基础。

LALR(1)项的DFA由下面方法得到:合并LR(1)项的DFA中具有相同核心的状态,并把对应先行符号求并集作为LALR(1)项的第2部分。这种合并会不会引入冲突呢?结论是:不会引入shift/reduce冲突,但可能引入reduce/reduce冲突。 (参见:编译原理第2版,4.7.4节)。

和前面一样,仍然考虑文法:A -> (A)|a,它的LR(1)项的DFA在前面已经给出,它的LALR(1)项的DFA如下图,显示它比LR(1)项对应DFA的状态数要少。

cfg_LALR1_DFA.png

Figure 27: 文法“A -> (A) | a”的LALR(1)项对应的DFA

1.5.2 LALR(1)和LR(1)的区别

SLR, LALR and LR parsers can all be implemented using exactly the same table-driven machinery.
The difference between LALR and LR has to do with the table generator. LR parser generators keep track of all possible reductions from specific states and their precise lookahead set; you end up with states in which every reduction is associated with its exact lookahead set from its left context. This tends to build rather large sets of states. LALR parser generators are willing to combine states if the GOTO tables and lookahead sets for reductions are compatible and don't conflict; this produces considerably smaller numbers of states, at the price of not be able to distinguish certain symbol sequences that LR can distinguish. So, LR parsers can parse a larger set of languages than LALR parsers, but have very much bigger parser tables. In practice, one can find LALR grammars which are close enough to the target languages that the size of the state machine is worth optimizing; the places where the LR parser would be better is handled by ad hoc checking outside the parser.

摘自:http://stackoverflow.com/questions/2676144/what-is-the-difference-between-lr-slr-and-lalr-parsers

1.5.3 是LR(1)但不是LALR(1)的例子

编译原理第2版,例4.58是一个典型的例子。

下面再给出一个和上书中类似的是LR(1)但不是LALR(1)的例子:
It is well known that there are grammars which are LR(1) but not LALR(1). For exemple:

 S -> aEa | bEb | aFb | bFa
 E -> e
 F -> e

Why? Please refer to:
http://stackoverflow.com/questions/8496065/why-is-this-lr1-grammar-not-lalr1
http://compilers.iecc.com/comparch/article/03-11-026

1.5.4 LALR(1)分析表构造

从理论上讲,构造LALR(1)分析表非常简单,合并LR(1)分析表中相同核心的项,并把先行符号求并集即可。
但问题是对于实用的文法来说,LR(1)分析表一般都非常庞大,无法完整地构造出LR(1)分析表(当然随着内存容量越来越大,完整构造LR(1)已成为可能)。
LALR(1)分析表构造算法有很多,Parsing Techniques,2nd一书中对其进行了总结。

典型的有:“Aho和Ullman提出的方法(他们都是龙书的作者,该方法在AT&T Yacc中使用)”和“DeRemer和Pennello提出的方法(该方法在Gnu Bison中使用)”。

1.5.5 构造LALR(1)分析表的经典算法——Yacc,Lemon所用算法

这种方法通过“传播和自发生成”的过程来生成先行符号,直接从LR(0)项的DFA得到LALR(1)项的DFA。
什么是自发生成的向前看符号?什么是传播来的向前看符号?

cfg_LALR1_lookahead.jpg

Figure 28: “自发生成的”和“传播来的”向前看符号(摘自《编译原理,第2版》4.7.5节)

怎么得到“自发生成的”和“传播来的”向前看符号呢?

cfg_LALR1_gen_lookahead.png

Figure 29: 发现“自发生成的”和“传播来的”向前看符号(摘自《编译原理,第2版》4.7.5节)

构造LALR(1)分析表的算法如下所示:

cfg_LALR1_construct_parsing_tab.jpg

Figure 30: 构造LALR(1)分析表(摘自《编译原理,第2版》4.7.5节,算法4.63)

它的主要思想是: 首先计算出所有项的产生式中“自发生成”的先行符号以及先行符号的传播途径(这个传播途径可方便地用表格表示,实例见“编译原理,第2版”4.7.4节P175,例4.64中的图4-46),之后用传播的方法增加先行符号。

1.5.5.1 为什么要把先行符号分为自发生成的和传播的?

把LALR(1)项的先行符号分为自发生成的和传播来的,这充分利用了LALR(1)项先行符号的下面特点:
(1) 传播关系不取决于某个特定的先行符号,也就是说要么所有的先行符号都从一个项传播到另一个项,要么都不传播。
(2) 传播来的先行符号,既然是从上一个项是传播过来的,那么它最开始也源于自发生成的先行符号。
由上面两点知, 只要知道自发生成的先行符号和先行符号的传播途径,就能推算出所有先行符号。

1.5.6 更快地构造LALR(1)分析表——Bison所用算法

参考:Efficient Computation of LALR(1) Look-Ahead Sets(Frank DeRemer, Thomas Pennello, 1982)

1.6 实例:从LR(0)到SLR到LR(1)

下面例子摘自:Parsing Techniques - A Practical Guide,2nd

cfg_ex1_grammar.png

cfg_ex1_NFA.png

cfg_ex1_inadequate_LR0_automaton.jpg

cfg_ex1_SLR1_automaton.jpg

cfg_ex1_LR1_automaton1.jpg

cfg_ex1_LR1_automaton2.jpg

cfg_ex1_LALR1_automaton.jpg

2 Bottom-up和Top-down的比较

Bottom-up(LR)和Top-down(LL)分析树的建立过程如图 38 所示。

cfg_LL_vs_LR.png

Figure 38: Bottom-up和Top-down建立分析树过程(摘自:https://en.wikipedia.org/wiki/Bottom-up_parsing

下面引用两处关于Bottom-up和Top-down的比较。

Bottom-up和Top-down的比较一,如图 39 所示(摘自:http://www.montefiore.ulg.ac.be/~geurts/Cours/compil/2011/03-syntaxanalysis-3.pdf)。

cfg_Bottom-up_vs_Top-down.jpg

Figure 39: Top-down versus Bottom-up parsing

Bottom-up和Top-down的比较二:

LL or LR?
This question has already been answered much better by someone else, so I'm just quoting his news message in full here:
I hope this doesn't start a war…
First - - Frank, if you see this, don't shoot me. (My boss is Frank DeRemer, the creator of LALR parsing…)
(I borrowed this summary from Fischer&LeBlanc's "Crafting a Compiler")

Simplicity - - LL
Generality - - LALR
Actions - - LL
Error repair - - LL
Table sizes - - LL
Parsing speed - - comparable (me: and tool-dependent)

Simplicity - - LL wins
========
The workings of an LL parser are much simpler. And, if you have to debug a parser, looking at a recursive-descent parser (a common way to program an LL parser) is much simpler than the tables of a LALR parser.

Generality - - LALR wins
========
For ease of specification, LALR wins hands down. The big difference here between LL and (LA)LR is that in an LL grammar you must left-factor rules and remove left recursion.
Left factoring is necessary because LL parsing requires selecting an alternative based on a fixed number of input tokens.
Left recursion is problematic because a lookahead token of a rule is always in the lookahead token on that same rule. (Everything in set A is in set A…) This causes the rule to recurse forever and ever and ever and ever…
To see ways to convert LALR grammars to LL grammars, take a look at my page on it:
http://www.jguru.com/thetick/articles/lalrtoll.html
Many languages already have LALR grammars available, so you'd have to translate. If the language doesn't have a grammar available, then I'd say it's not really any harder to write a LL grammar from scratch. (You just have to be in the right "LL" mindset, which usually involves watching 8 hours of Dr. Who before writing the grammar… I actually prefer LL if you didn't know…)

Actions - - LL wins
=====
In an LL parser you can place actions anywhere you want without introducing a conflict

Error repair - - LL wins
==========
LL parsers have much better context information (they are top-down parsers) and therefore can help much more in repairing an error, not to mention reporting errors.

Table sizes - - LL
=========
Assuming you write a table-driven LL parser, its tables are nearly half the size. (To be fair, there are ways to optimize LALR tables to make them smaller, so I think this one washes…)

Parsing speed - comparable (me: and tool-dependent)

上面内容摘自:BNF and EBNF: What are they and how do they work?


Author: cig01

Created: <2013-01-19 Sat 00:00>

Last updated: <2017-12-09 Sat 13:57>

Creator: Emacs 25.3.1 (Org mode 9.1.4)