在开发CuteSqlite图形客户端的时候,需要用到SQL的语法解释,来对SQL语句进行优化。找了很多的SQL语法解释器,都不是十分满意,只有翻开Sqlite的源码,看看SQLite对SQL语句的解释过程,上一篇文章翻译了官方介绍SQLite架构,本文翻译了官方介绍Lemon解释器的文章。
官方介绍Lemon的文章:https://www.sqlite.org/src/doc/trunk/doc/lemon.html
中文翻译SQLite架构文章:开源项目CuteSqlite开发笔记(二):SQLite的架构
开源项目CuteSqlite网址:https://github.com/shinehanx/CuteSqlite.git
Lemon解释器
Lemon是C语言的LALR(1)解析生成器。它和“BISON”和“yacc”做同样的工作。但Lemon不是一个BISON或yacc克隆。Lemon使用不同的语法,旨在减少编码错误的数量。Lemon还使用了一个解析引擎,它比yacc和BISON更快,并且是可重入和线程安全的。(更新:自从写了上一句话,BISON也被更新了,这样它也可以生成可重入和线程安全的解析器。)Lemon还实现了可用于消除资源泄漏的功能,使其适用于长时间运行的程序,如图形用户界面或嵌入式控制器。
本文档是对Lemon解析器生成器的介绍。
1.0 目录
2.0安全说明
Lemon创建的语言解析器代码非常健壮,非常适合用于需要安全处理恶意输入的面向Internet的应用程序。
“lemon.exe”命令行工具本身在给定有效的输入语法文件时工作得很好,并且几乎总是为格式错误的输入提供有用的错误消息。但是,恶意用户可能会手工创建一个语法文件,导致lemon.exe崩溃。我们不认为这是一个问题,因为lemon.exe不打算用于恶意输入。总结如下:
- lemon生成的解析器代码→健壮且安全
- “lemon.exe”命令行工具本身→没有那么多
3.0操作理论
Lemon是一个计算机程序,它将特定语言的上下文无关语法(CFG)转换为C代码,实现该语言的解析器。Lemon程序有两个输入:
- 语法规范。
- 解析器模板文件。
通常,程序员只提供语法规范。Lemon附带了一个默认的解析器模板(“lempar.c”),它可以很好地用于大多数应用程序。但是如果需要,用户可以自由地替换不同的解析器模板。
根据命令行选项的不同,Lemon最多会生成三个输出文件。
- C代码来实现输入语法的解析器。
- 为每个终端符号(或“token”)定义整数ID的头文件。
- 描述生成的解析器自动机的状态的信息文件。
默认情况下,将生成所有这三个输出文件。如果使用“-m”命令行选项,则头文件将被抑制,而当选择“-q”时,报告文件将被省略。
按照惯例,语法规范文件使用“.y”后缀。在本文档中使用的示例中,我们假设语法文件的名称为“gram.y”。Lemon的典型用法是以下命令:
lemon gram.y
此命令将生成三个输出文件,分别名为“gram.c”、“gram.h”和“gram.out”。第一个是实现解析器的C代码。第二个是头文件,它定义了所有终端符号的数值,最后一个是报告,它解释了解析器自动机使用的状态。
3.1命令行选项
可以使用命令行选项修改Lemon的行为。您可以通过键入以下命令来获得可用命令行选项的列表以及每个选项的简要说明:
lemon "-?"
在撰写本文时,支持以下命令行选项:
- -b 仅显示报告文件中每个解析器状态的基础。
- -c 不压缩生成的操作表。解析器会更大更慢一些,但它会更快地检测到语法错误。
- -ddirectory 将所有输出文件写入目录。通常,输出文件被写入包含输入语法文件的目录中。
- -Dname 定义C预处理器宏名。语法文件中的“ %ifdef “、“ %ifndef “和“ %if "行可使用此宏。
- -E 仅运行“%if”预处理器步骤并打印修改后的语法文件。
- -g 不生成解析器。相反,将输入语法写入标准输出,并删除所有注释、操作和其他无关文本。
- -l 在生成的解析器C代码中省略“#line”指令。
- -m 使输出的C源代码与“makeheaders”程序兼容。
- -p 显示由优先级规则解决的所有冲突。
- -q 禁止生成报告文件。
- -r 不要将解析器状态排序或重新编号作为优化的一部分。
- -s 退出前显示解析器统计信息。
- -Tfile 使用file作为生成的C代码解析器实现的模板。
- -x 打印Lemon版本号。
3.2 解析器接口
Lemon不会生成一个完整的、可工作的程序。它只生成几个实现解析器的子例程。本节描述这些子程序的接口。这取决于程序员以适当的方式调用这些子程序,以产生一个完整的系统。
在程序开始使用Lemon生成的解析器之前,程序必须首先创建解析器。一个新的解析器创建如下:
void *pParser = ParseAlloc( malloc );
ParseAlloc()例程分配和调用一个新的解析器,并返回一个指向它的指针。用于表示解析器的实际数据结构是不透明的-它的内部结构不可见,也不能被调用例程使用。因此,ParseAlloc()例程返回一个指向void的指针,而不是指向某个特定结构的指针。ParseAlloc()例程的唯一参数是一个指向用于分配内存的子例程的指针。这通常意味着malloc()。
在程序使用完解析器之后,它可以通过调用
ParseFree(pParser, free);
第一个参数与ParseAlloc()返回的指针相同。第二个参数是一个指针,指向用于将大容量内存释放回系统的函数。
使用ParseAlloc()分配解析器后,程序员必须为解析器提供一个要解析的token(终端符)序列。这是通过为每个token调用以下函数一次来实现的:
Parse(pParser, hTokenID, sTokenData, pArg);
Parse()例程的第一个参数是ParseAlloc()返回的指针。第二个参数是一个小的正整数,它告诉解析器数据流中下一个token的类型。语法中的每个终端符号(terminal symbol)都有一个token类型。Lemon生成的gram.h文件包含#define语句,这些语句将符号终端符号(symbolic terminal symbol)名称映射为适当的整数值。第二个参数的值0是解析器的一个特殊标志,表示已经到达输入的结尾。第三个参数是给定token的值。默认情况下,第三个参数的类型是“void*",但语法通常会将此类型重新定义为某种structure。通常,第二个参数将是一个广泛的token类别,如“标识符”或“数字”,第三个参数将是token的名称或数字的值。
Parse()函数可以有三个或四个参数,这取决于语法。如果语法规范文件请求它(通过 %extra_argument 指令),Parse()函数将有第四个参数,可以是程序员选择的任何类型。解析器不对这个参数做任何事情,只是将它传递给action例程。这是一种可以将状态信息传递给action例程(action routines)的方便的机制,而不必使用全局变量。
Lemon解析器的典型用法可能如下所示:
1 ParseTree *ParseFile(const char *zFilename){
2 Tokenizer *pTokenizer;
3 void *pParser;
4 Token sToken;
5 int hTokenId;
6 ParserState sState;
7
8 pTokenizer = TokenizerCreate(zFilename);
9 pParser = ParseAlloc( malloc );
10 InitParserState(&sState);
11 while( GetNextToken(pTokenizer, &hTokenId, &sToken) ){
12 Parse(pParser, hTokenId, sToken, &sState);
13 }
14 Parse(pParser, 0, sToken, &sState);
15 ParseFree(pParser, free );
16 TokenizerFree(pTokenizer);
17 return sState.treeRoot;
18 }
这个例子展示了一个用户编写的例程,它解析一个文本文件并返回一个指向解析树的指针。(为了保持简单,本例中省略了所有错误处理代码。)我们假设存在某种tokenizer,它是在第8行使用TokenizerCreate()创建的,并在第16行被TokenizerFree()删除。第11行的GetNextToken()函数从输入文件中检索下一个token,并将其类型放入整数变量hTokenId中。假设sToken变量是某种结构体(structure),它包含每个token的详细信息,例如它的完整文本,它出现在哪一行,等等。
此示例还假设存在一个ParserState类型的结构体,该结构保存有关特定解析的状态信息。这样一个结构的实例在第6行创建,并在第10行初始化。指向此结构的指针作为可选的第四个参数传递到Parse()例程中。语法为解析器指定的action例程可以使用ParserState结构来保存任何有用和适当的信息。在这个例子中,我们注意到ParserState结构的treeRoot字段指向解析树的根。
这个例子的核心,因为它涉及Lemon如下:
ParseFile(){
pParser = ParseAlloc( malloc );
while( GetNextToken(pTokenizer,&hTokenId, &sToken) ){
Parse(pParser, hTokenId, sToken);
}
Parse(pParser, 0, sToken);
ParseFree(pParser, free );
}
基本上,程序要使用Lemon生成的解析器,首先要创建解析器,然后通过token格式化后的输入源,向它发送获得的大量token。当到达输入的结尾时,Parse()例程应该最后一次被调用,token类型为0。这一步对于通知解析器已经到达输入的结尾是必要的。最后,我们通过调用ParseFree()回收解析器使用的内存。
在我们继续之前,应该提到另一个接口例程,ParseTrace()函数可用于从解析器生成调试输出。此例程的原型如下:
ParseTrace(FILE *stream, char *zPrefix);
在调用这个例程之后,每当解析器改变状态或调用action例程时,都会向指定的输出流写入一条简短的(一行)消息。每个这样的消息都使用zPrefix给出的文本作为开头。可以通过再次调用ParseTrace()并将第一个参数设置为NULL(0)来关闭此调试输出。
3.2.1在堆栈上分配Parse对象
如果所有对Parse()接口的调用都是从 %code 指令中进行的,那么可以从堆栈而不是从堆中分配parse对象。这些是步骤:
- 声明一个类型为“yyParser”的局部变量
- 使用ParseInit()初始化变量
- 在Parse()调用中传递指向变量的指针
- 使用ParseFinalize()在parse变量中释放子结构。
下面的代码演示了如何做到这一点:
ParseFile(){
yyParser x;
ParseInit( &x );
while( GetNextToken(pTokenizer,&hTokenId, &sToken) ){
Parse(&x, hTokenId, sToken);
}
Parse(&x, 0, sToken);
ParseFinalize( &x );
}
3.2.2接口摘要
下面是对Lemon生成的解析器的C语言接口的快速概述:
void *ParseAlloc( (void*(*malloc)(size_t) ); void ParseFree(void *pParser, (void(*free)(void*) ); void Parse(void *pParser, int tokenCode, ParseTOKENTYPE token, ...); void ParseTrace(FILE *stream, char *zPrefix);
备注:
- 使用 %name 指令更改接口中过程的“Parse”前缀名称。
- 使用 %token_type 指令定义“ParseTOKENTYPE”类型。
- 使用 %extra_argument 指令指定Parse()函数的第四个参数的类型和名称。
3.3与YACC和BISON的区别
以前使用过yacc或bison解析器的程序员会注意到yacc/bison与Lemon之间的几个重要区别。
- 在yacc和bison中,解析器(Parser)调用标记器(Tokenizer)。在Lemon中,标记器(Tokenizer)调用解析器(Parser)。
- Lemon不使用全局变量。Yacc和bison使用全局变量在标记器(Tokenizer)和解析器(Parser)之间传递信息。
- Lemon允许多个解析器同时运行。Yacc和bison没有。
这些差异可能会使具有yacc和bison经验的程序员产生一些最初的混淆。但经过多年使用Lemon的经验,我坚信Lemon的做事方式更好。
2016-02-16更新:以上文字写于20世纪90年代。我们被告知,bison最近得到了增强,以支持Lemon使用的标记器(Tokenizer)-调用-解析器(Parser)范式,消除了对全局变量的需求。
3.4构建“lemon”或“lemon.exe”可执行文件
“lemon”或“lemon.exe”程序是从一个名为“lemon.c”的C代码文件构建的(使用Sqlite3工程源码的话,tools目录中有该文件)。Lemon源代码是通用的C89代码,没有使用任何不寻常或非标准的库。任何合理的C编译器都足以编译lemon程序。像下面这样的命令行通常会工作:
cc -o lemon lemon.c
在安装了Visual C++的Windows计算机上,打开“x64 Native Tools Command Prompt for VS 20xx”窗口并输入:
cl lemon.c
编译Lemon就是这么简单。如果需要,可以添加其他编译器选项,如“-O2”或“-g”或“-Wall”,但它们不是必需的。
4.0输入文件语法
Lemon语法规范文件的主要目的是为解析器定义语法。但是输入文件还指定了Lemon完成其工作所需的其他信息。使用Lemon的大部分工作是编写适当的语法文件。
Lemon的语法文件在很大程度上是一种自由格式。它没有像yacc或bison那样的章节(sections)或划分(divisions)。任何声明都可以出现在文件中的任何位置。Lemon忽略空白(除非需要分隔token),并且它遵循与C和C++相同的注释约定。
4.1终端符(Terminals)和非终端符(Nonterminals)
终端符(token)是以大写字母开头的任何字母数字和/或下划线字符串。一个终端符可以在第一个字符后包含小写字母,但通常的约定是使终端符都是大写字母。另一方面,非终端符是以小写字母开头的任何字母数字和下划线字符串。同样,通常的约定是让非终端符使用所有的小写字母。
在Lemon中,终端符和非终端符不需要在语法文件的单独部分中声明或标识。Lemon能够通过检查语法规则,来生成所有终端符和非终端符的列表,并且,它总是可以通过检查名称的第一个字符的大小写,来区分终端符和非终端符。
Yacc和bison允许终端符具有字母数字名称或包含在单引号中的单个字符,如')'或'$'。Lemon不允许这种替代形式的终端符。使用Lemon,所有符号(终端符和非终端符)都必须具有字母数字名称。
4.2语法规则
Lemon语法文件的主要组成部分是一系列语法规则。每个语法规则都由一个非终端符和一个特殊符号"::="组成,然后是一个终端符和/或非终端符列表。该规则由英文句号终止。规则右侧的终端符和非终端符列表可以为空。规则可以以任何顺序出现,除了第一个规则的左侧被假定为语法的开始符号(除非使用下面描述的 %start_symbol 指令另有指定)。一个典型的语法规则序列可能看起来像这样:
expr ::= expr PLUS expr.
expr ::= expr TIMES expr.
expr ::= LPAREN expr RPAREN.
expr ::= VALUE.
在这个例子中有一个非终端符,"expr",和五个终端符或token:"PLUS","TIMES","LPAREN","RPAREN"和"VALUE"。
像yacc和bison一样,Lemon允许语法指定一个C代码块,每当语法规则被解析器简化时,该代码块将被执行。在Lemon中,通过将C代码(包含在花括号 {...} 中)直接放在关闭规则的句点之后来指定此操作。举例来说:
expr ::= expr PLUS expr. { printf("Doing an addition...\n"); }
为了方便使用,语法动作(grammar actions)通常必须与其相关的语法规则相关联。在yacc和bison中,这是通过在action中嵌入“$$”来表示规则左侧的值,并在规则右侧的位置1、2等处嵌入符号“$1”、“$2”等来表示终端符或非终端符的值来实现的。这个idea非常强大,但也非常容易出错。yacc或bison语法中最常见的一个错误来源是错误地计算了语法规则右侧的符号数量,当你真正想说的是“$8”时,你说的是“$7”。
Lemon通过在语法规则中为每个符号分配符号名称,然后在action中使用这些符号名称,从而避免了计算语法符号的需要。在yacc或bison中,可以这样写:
expr -> expr PLUS expr { $$ = $1 + $3; };
但在Lemon中,同样的规则变成了以下内容:
expr(A) ::= expr(B) PLUS expr(C). { A = B+C; }
在Lemon规则中,语法规则符号后面括号中的任何符号,都将成为该符号在语法规则中的占位符。这个占位符可以在相关的C语言action中用来代表该符号的值。
用于连接语法规则(linking a grammar rule)和其reduce action的Lemon表示法在几个方面优于yacc/bison。首先,如上所述,Lemon方法避免了计算语法符号的需要。其次,如果Lemon语法规则中的终端符或非终端符在括号中包含连接符号(linking symbol),但该链接符号实际上并未在reduce action中使用,则会生成错误消息。例如,规则
expr(A) ::= expr(B) PLUS expr(C). { A = B; }
将生成错误,因为语法规则中使用了链接符号“C”,而reduce action中没有使用。
用于将语法规则连接到reduce action的Lemon表示法,也便于使用析构函数来回收由规则右侧的终端符和非终端符的值分配的内存。
4.3优先规则
Lemon解决解析二义性的方式与yacc和bison完全相同。shift-reduce冲突(shift-reduce conflict)通过shift来解决,而reduce-reduce冲突(reduce-reduce conflict)通过reduce语法文件中最先出现的规则来解决。
就像在yacc和bison中一样,Lemon允许使用优先级规则来控制解析冲突的解决。可以使用 %left 、 %right 或 %nonassoc 指令将优先级值分配给任终端符。早出现的终端符的优先级比在晚出现的终端符的优先级低。举例来说:
%left AND.
%left OR.
%nonassoc EQ NE GT GE LT LE.
%left PLUS MINUS.
%left TIMES DIVIDE MOD.
%right EXP NOT.
在前面的指令序列中,AND运算符被定义为具有最低的优先级。OR运算符的优先级要高一级。诸如此类。因此,语法会尝试将歧义表达式分组
a AND b OR c
像这样
a AND (b OR c).
结合性(左、右或非结合性)用于确定优先级相同时的分组。在我们的例子中AND是左结合的,所以
a AND b AND c
是这样解析的
(a AND b) AND c.
不过,EXP运算符是右关联的,所以
a EXP b EXP c
是这样解析的
a EXP (b EXP c).
非关联优先级用于非关联运算符(在%nonassoc 中指定的运算符) 。所以
a EQ b EQ c
会发生错误
非终端符(non-terminals)的优先级被转移到如下规则:语法规则的优先级等于定义了优先级的规则中最左边的终端符的优先级。这通常是你想要的,但是在那些你想要一个语法规则的优先级是不同的情况下,你可以指定一个替代的优先级符号,方法是把这个符号放在规则末尾的句号之后和任何C代码之前的方括号中。举例来说:
expr = MINUS expr. [NOT]
此规则的优先级等于NOT符号,而不是默认情况下的MINUS符号。
有了优先级如何分配给终端符和单个语法规则的知识,我们现在可以精确地解释解析冲突如何在Lemon中解决。Shift-Reduce冲突的解决方法如下:
- 如果Shift token或要reduce的规则缺少优先级信息,则进行有利于Shift的解析,但报告解析冲突。
- 如果Shift token的优先级大于reduce规则的优先级,则采用Shift。不报告分析冲突。
- 如果Shift token的优先级小于要reduce规则的优先级,则解析以支持reduce操作。不报告分析冲突。
- 如果优先级是相同的,并且Shift token是右关联的,那么就选择Shift。不报告分析冲突。
- 如果优先级是相同的,并且Shift token是左关联的,那么就选择reduce。不报告分析冲突。
- 否则,通过执行Shift来解决冲突,并报告解析冲突。
Reduce-reduce冲突是这样解决的:
- 如果任一reduce规则缺少优先级信息,则采用语法中最先出现的规则进行解析,并报告解析冲突。
- 如果两个规则都有优先级,并且优先级不同,则以优先级最高的规则解决争议,并且不报告冲突。
- 否则,通过按语法中最先出现的规则进行reducing来解决冲突,并报告分析冲突。
4.4特别指示
Lemon的输入语法由语法规则和特殊指令组成。我们已经描述了所有的语法规则,现在我们将讨论特殊指令。
Lemon中的指令可以以任何顺序出现。你可以把它们放在语法规则的前面,或者后面,或者中间。给终端符分配优先级的指令的相对顺序很重要(4.3节里有说明),但除此之外,Lemon中指令的顺序是任意的。
Lemon支持以下特殊指令:
- %code
- %default_destructor
- %default_type
- %destructor
- %else
- %endif
- %extra_argument
- %fallback
- %if
- %ifdef
- %ifndef
- %include
- %left
- %name
- %nonassoc
- %parse_accept
- %parse_failure
- %right
- %stack_overflow
- %stack_size
- %start_symbol
- %syntax_error
- %token
- %token_class
- %token_destructor
- %token_prefix
- %token_type
- %type
- %wildcard
以下章节将分别介绍这些指令:
4.4.1 %code 指令
%code 指令用于指定添加到主输出文件末尾的其他C代码。这类似于 %include 指令,除了 %include 被插入到主输出文件的开头。
%code 通常用于包含一些action例程或标记器(Tokenizer),甚至是“main()”函数作为输出文件的一部分。
可以有多个 %code 指令。所有 %code 指令的参数都连接在一起。
4.4.2 %default_destructor 指令
%default_destructor 指令指定了一个析构函数,用于没有单独的 %destructor 指令指定其自己的析构函数的非终端符(non-termimal)。有关更多信息,请参阅下面关于 %destructor 指令的文档。
在某些语法中,许多不同的非终端符具有相同的数据类型,因此具有相同的析构函数。该指令是一种方便的方法,可以使用一条语句为所有非终端符指定相同的析构函数。
4.4.3 %default_type 指令
%default_type 指令指定了没有使用单独的 %type 指令定义自己的数据类型的非终端符的数据类型。
4.4.4 %destructor 指令
%destructor 指令用于指定非终端符的析构函数。( %token_destructor 指令,用于指定终端符的析构函数。)
每当非终端符从堆栈中弹出时,都会调用非终端符的析构函数来释放非终端符的值。这包括以下所有情况:
- 当规则Reduce并且右侧的非终端符的值没有链接到C代码时。
- 在错误处理期间弹出堆栈时。
- 当ParseFree()函数运行时。
析构函数可以对非终端符的值做任何它想做的事情,但它的设计是释放非终端符所持有的内存或其他资源。
举个例子:
%type nt {void*}
%destructor nt { free($$); }
nt(A) ::= ID NUM. { A = malloc( 100 ); }
这个例子有点做作,但它用来说明析构函数是如何工作的。该示例显示了一个名为“nt”的非终端符,它保存的值类型为“void*"。当“nt”的规则reduce时,它将非终端符的值设置为从malloc()获得的空格。稍后,当nt非终端符从堆栈中弹出时,析构函数将触发并在这个错误的空间上调用free(),从而避免内存泄漏。(注意析构函数代码中的符号“$$”被非终端符的值替换。)
重要提醒的是,每当从堆栈中删除非终端符时,非终端符的值都会传递给析构函数,除非非终端符用于C代码操作。如果非终端符被C代码使用,那么C代码会负责销毁它。更常见的情况是,该值用于构建一些更大的结构,我们不想销毁它,这就是为什么在这种情况下不调用析构函数。
析构函数通过在分配的对象超出作用域时自动释放它们来帮助避免内存泄漏。使用yacc或bison做同样的事情要困难得多。
4.4.5 %extra_argument 指令
%extra_argument 指令指示Lemon将第四个参数添加到它生成的Parse()函数的参数列表中。Lemon本身不对这个额外的参数做任何事情,但它确实使该参数可用于C代码操作例程、析构函数等。例如,如果语法文件包含:
%extra_argument { MyStruct *pAbc }
然后,生成的Parse()函数将具有类型为“MyStruct*”的第四个参数,并且所有action例程将可以访问名为“pAbc”的变量,该变量是最近对Parse()的调用中的第四个参数的值。
%extra_context 指令的工作原理相同,只是它是在ParseAlloc()或ParseInit()例程中传递的,而不是在Parse()中。
4.4.6 %extra_context 指令
%extra_context 指令指示Lemon向ParseAlloc()和ParseInit()函数的参数列表添加第二个参数。Lemon本身不对这些额外的参数做任何事情,但它确实存储了值,使其可用于C代码操作例程、析构函数等。例如,如果语法文件包含:
%extra_context { MyStruct *pAbc }
然后ParseAlloc()和ParseInit()函数将具有类型为“MyStruct*”的第二个参数,并且所有action例程将能够访问名为“pAbc”的变量,该变量是第二个参数的值。
%extra_argument 指令的工作原理相同,只是它是在Parse()例程中传递的,而不是在ParseAlloc()/ParseInit()中。
4.4.7 %fallback 指令
%fallback 指令指定一个或多个Token的替代含义。如果原始Token会生成语法错误,则尝试替代含义。
添加 %fallback 指令是为了支持SQLite中SQL语法的健壮解析。SQL语言包含大量的关键字,每个关键字对于语言解析器来说都是不同的Token。SQL包含如此多的关键字,以至于程序员很难跟上它们。因此,程序员有时会错误地使用模糊的语言关键字作为标识符。 %fallback 指令提供了一种机制来告诉解析器:“如果您无法解析此关键字,请尝试将其视为标识符。“
%fallback 的语法如下:
%fallback ID TOKEN... .
换句话说, %fallback 指令后面是一个以句点结尾的Token名称列表。第一个Token名称是回退Token-所有其他Tokens回退到的Token。第二个和随后的参数是返回到第一个参数所标识的Token
4.4.8 %if 指令及其朋友
%if 、 %ifdef 、 %ifndef 、 %else 和 %endif 指令类似于C预处理器中的#if、#ifdef、#ifndef、#else和#endif,只是不那么通用。这些指令中的每一个都必须从左边距开始开始。“%”和指令名称之间不允许有空格。
除非使用“-DMACRO”命令行选项,否则将忽略“ %ifdef MACRO “和下一个嵌套“ %endif “之间的语法文本。除非使用“-DMACRO”命令行选项,否则包括“ %ifndef MACRO “和下一个嵌套“ %endif “之间的语法文本。
仅当CONDITIONAL为真时,才会包含“ %if CONDITIONAL”及其对应的 %endif 之间的文本。CONDITION是一个或多个宏名,可以选择使用“||和“&&”二元运算符,则“!“一元运算符,并使用平衡括号进行分组。如果对应的宏存在,则每个项为true,如果不存在,则为false。
可选的“ %else “指令可以出现在 %ifdef 、 %ifndef 或 %if 指令及其对应的 %endif 之间的任何位置。
请注意, %ifdef 和 %ifndef 的参数旨在作为单个预处理器符号名称,而不是一般表达式。对于一般表达式使用“ %if “指令。
4.4.9 %include 指令
%include 指令指定包含在生成的解析器顶部的C代码。您可以包含任何您想要的文本-Lemon解析器生成器盲目地复制它。如果你的语法文件中有多个 %include 指令,它们的值会被连接起来,这样所有的 %include 代码最终都会出现在生成的解析器的顶部附近,顺序与它在语法中出现的顺序相同。
在生成的解析器的开头,使用 %include 指令可以很方便地获取一些额外的#include预处理器语句。举例来说:
%include {#include <unistd.h>}
例如,如果语法中的某些C操作调用unistd. h中原型化的函数,则可能需要这样做。
使用 %code 指令将代码添加到生成的解析器的末尾。
4.4.10 %left 指令
使用 %left 指令(沿着使用 %right 和 %nonassoc 指令)来声明终端符的优先级。其名称出现在 %left 指令之后但在下一个句点(".") 之前的每个终端符。被赋予相同的左关联优先级值。后续的 %left 指令具有更高的优先级。举例来说:
%left AND.
%left OR.
%nonassoc EQ NE GT GE LT LE.
%left PLUS MINUS.
%left TIMES DIVIDE MOD.
%right EXP NOT.
请注意,每个 %left 、 %right 或 %nonassoc 指令的终止时间。
LALR(1) 语法可能会陷入这样一种情况:如果大量使用右结合运算符,它们需要大量的堆栈空间。因此,建议您尽可能使用 %left 而不是 %right 。
4.4.11 %name 指令
默认情况下,Lemon生成的函数都以五个字符的字符串“Parse”开始。您可以使用 %name 指令将此字符串更改为其他字符串。例如:
%name Abcde
将此指令放在语法文件中将导致Lemon生成名为
- AbcdeAlloc(),
- AbcdeFree(),
- AbcdeTrace(), and
- Abcde().
%name 指令允许您生成两个或多个不同的 解析器并将它们全部链接到同一个可执行文件中。
4.4.12 %nonassoc 指令
此指令用于为一个或多个终端符指定非关联优先级。有关更多信息,请参见优先级规则或 %left 指令部分。
4.4.13 %parse_accept 指令
%parse_accept 指令指定了一个C代码块,每当解析器接受它的输入字符串时,它就会被执行。“接受”一个输入字符串意味着解析器能够正确地处理所有的token。
For example:
%parse_accept {
printf("parsing complete!\n");
}
4.4.14 %parse_failure 指令
%parse_failure 指令指定了一个C代码块,每当解析器失败时,它就会被执行。在解析器尝试使用通常的错误恢复策略解决输入错误失败之前,不会执行此代码。只有在解析无法继续时才会调用该例程。
%parse_failure {
fprintf(stderr,"Giving up. Parser is hopelessly lost...\n");
}
4.4.15 %right 指令
此指令用于为一个或多个终端符指定右关联优先级。有关其他信息,请参阅优先级规则或%left指令部分。
4.4.16 %stack_overflow 指令
%stack_overflow 指令指定了一个C代码块,如果解析器的内部堆栈溢出,则执行该代码块。通常,这只是打印一条错误消息。堆栈溢出后,分析器将无法继续,必须重置。
%stack_overflow {
fprintf(stderr,"Giving up. Parser stack overflow\n");
}
通过避免在语法中使用右递归和右优先运算符,可以帮助防止分析器堆栈溢出。使用左递归和和左优先操作符来鼓励规则更快地Reduce并保持堆栈大小。例如,执行如下规则:
list ::= list element. // left-recursion. Good!
list ::= .
Not like this:
list ::= element list. // right-recursion. Bad!
list ::= .
4.4.17 %stack_size 指令
如果堆栈溢出是一个问题,而使用左递归无法解决这个问题,那么您可能希望使用此指令增加解析器堆栈的大小。在 %stack_size 指令后放置一个正整数,Lemon将生成一个具有所请求大小的堆栈的解析。默认值为100。
%stack_size 2000
4.4.18 %start_symbol 指令
默认情况下,Lemon 生成的语法的开始符号 是语法文件中出现的第一个非终端。但是你 可以使用 %start_symbol 指令选择不同的开始符号。
%start_symbol prog
4.4.19 %syntax_error 指令
See Error Processing. 请参见错误处理。
4.4.20 %token 指令
token通常在第一次使用时自动创建。任何以大写字母开头的标识符都是token。
但是,有时候提前声明token是有用的。分配给每个token的整数值,由token的显示顺序决定。因此,通过提前声明token,可以使某些token具有低编号的值,这在某些语法中可能是期望的,或者将顺序值分配给相关token的序列。出于这个原因,提供了%token指令来提前声明token。语法如下:
%token TOKEN TOKEN... .
%token指令后面跟着零个或多个token符号,并以单个“结束。“.如果名为的每个token尚不存在,则将创建该token。token是按顺序创建的。
4.4.21 %token_class 指令
Undocumented. Appears to be related to the MULTITERMINAL concept. Implementation.
无记录。这似乎与多元概念有关。实施.
4.4.22 %token_destructor 指令
%destructor 指令将析构函数分配给非终端符。(See上面的 %destructor 指令的描述。) %token_destructor 指令对所有终端符号执行相同的操作。
与非终端符符号不同,它们的值可能具有不同的数据类型,终端符都使用相同的数据类型(由 %token_type 指令定义),因此它们使用公共析构函数。除此之外,token析构函数的工作方式和非终端符析构函数一样。
4.4.23 %token_prefix 指令
Lemon生成#defines,为语法中的每个终端符分配小整数常量。如果需要,Lemon会将此指令指定的前缀添加到它生成的每个#defines中。
因此,如果Lemon的默认输出看起来像这样:
#define AND 1
#define MINUS 2
#define OR 3
#define PLUS 4
你可以像这样在语法中插入一个语句:
%token_prefix TOKEN_
让 Lemon产生这些符号
#define TOKEN_AND 1
#define TOKEN_MINUS 2
#define TOKEN_OR 3
#define TOKEN_PLUS 4
4.4.24 %token_type 和 %type 指令
这些指令用于指定解析器堆栈上与终端符和非终端符相关的值的数据类型。所有终端符的值必须为同一类型。这与Lemon生成的Parse()函数的第三个参数的数据类型相同。通常,您将使终端符号的值成为指向某种token结构的指针。就像这样:
%token_type {Token*}
如果未指定终端符的数据类型,则默认值为“void*"。
每个非终端符都可以有自己的数据类型。通常,非终端符的数据类型是指向解析树结构的根的指针,该解析树结构包含有关该非终端符的所有信息。举例来说:
%type expr {Expr*}
解析器堆栈上的每个条目实际上都是一个union,其中包含每个非终端符和终端符的所有数据类型的实例。Lemon将自动使用这个union的正确元素,这取决于相应的非终端符或终端符是什么。但是语法设计者应该记住,union的大小将是其最大元素的大小。因此,如果你有一个非终端符,其数据类型需要1K的存储空间,那么你的100个条目的解析器堆栈将需要100K的堆空间。如果你愿意并且能够支付这个价格,那就好。你只需要知道。
4.4.25 %wildcard 指令
%wildcard 指令后面是一个token名和一个句点。该指令指定标识的token应与任何输入token匹配。
当生成的解析器可以选择将一个输入与一个token和另一个token进行匹配时,总是使用另一个token。只有在没有其他选项的情况下,才匹配该token。
5.0错误处理
经过几年的大量实验,我们发现yacc使用的错误恢复策略已经达到了最佳效果。这就是Lemon所使用的。
当Lemon生成的解析器遇到语法错误时,它首先调用 %syntax_error 指令指定的代码(如果有的话)。然后,它进入其错误恢复策略。错误恢复策略是开始弹出解析器堆栈,直到它进入允许移动名为“error”的特殊非终端符的状态。然后它移动这个非终端符并继续解析。在至少三个新token成功转移之前,不会再次调用 %syntax_error 例程。
如果解析器弹出其堆栈直到堆栈为空,并且它仍然不能移动错误符号,则调用 %parse_failure 例程,并且解析器将其自身重置为开始状态,准备开始解析新文件。当然,如果你的语法中没有“error”非终端符的实例,这就是第一个语法错误时会发生的情况。
6.0Lemon的历史
Lemon最初是由Richard Hipp在20世纪80年代后期的某个时候在Sun4工作站上使用K&R C编写的。有一个配套的LL(1)解析器生成器程序,名为“Lime”。Lime的源代码已经丢失。
lemon. c源文件最初是许多单独的文件,它们被编译在一起以生成“lemon”可执行文件。在20世纪90年代的某个时候,各个源代码文件被合并到当前的单个大型“lemon. c”源文件中。您仍然可以在代码中看到原始文件名的痕迹。
自2001年以来,Lemon一直是SQLite项目的一部分,Lemon的源代码作为SQLite源代码树的一部分在以下文件中进行管理:
7.0 Copyright
Lemon的所有源代码,包括模板解析器文件“lempar.c”和这个文档文件(“lemon.html”)都在公共领域。您可以将代码用于任何目的,而无需归因。
代码没有保修。如果它坏了,你就可以保留两块。