atitit.词法分析原理 词法分析器 (Lexer)
atitit.词法分析原理 词法分析器 (Lexer)
2. ;实现词法分析程序的常用途径:自动生成,手工生成.[1] 2
2.4. Token的类型,根据程序设计语言的特点,单词可以分为五类:关键字、标识符、常量、运算符、界符。以4
2.10. 不确定”(Nondeterministic Finite Automata ,NFA 8
2.11. 转换图(transition graph)的表示9
2.15. 正则表达式如何转换为NFA呢?有几个公式(MLS2007[1]):13
1. 词法分析(英语:lexical analysis)
是计算机科学中将字符序列转换为单词(Token)序列的过程。进行词法分析的程序或者函数叫作词法分析器(Lexical analyzer,简称Lexer),也叫扫描器(Scanner
词法分析阶段是编译过程的第一个阶段,是编译的基础。这个阶段的任务是从左到右一个字符一个字符地读入源程序,即对构成源程序的字符流进行扫描然后根据构词规则识别单词(也称单词符号或符号)。词法分析程序实现这个任务。词法分析程序可以使用Lex等工具自动生成。
词法分析是编译程序的第一个阶段且是必要阶段;词法分析的核心任务是扫描、识别单词且对识别出的单词给出定性、定长的处理
一段对计算机来说豪无意义的字符串,经过语法分析后就得到了略微有意义的 Token 流。digit 就表示这个词法单元对应的是数字,operator 则表示操作符,后面相应的数字和符号(粉色背景)就是词素。同时,程序中一些不必要的空白、注释也可以由词法分析器来过滤掉,这样,之后的语法分析等步骤处理起来就会容易得多
作者:: ★(attilax)>>> 绰号:老哇的爪子 ( 全名::Attilax Akbar Al Rapanui 阿提拉克斯 阿克巴 阿尔 拉帕努伊 ) 汉字名:艾龙, EMAIL:1466519819@qq.com
转载请注明来源: http://blog.csdn.net/attilax
2. ;实现词法分析程序的常用途径:自动生成,手工生成.[1]
尽管在某些情况下需要手工编写词法分析器,使用状态模式,,一般情况下词法分析器都用自动化工具生成。
2.1. 词法分析程序的功能
完成词法分析任务的程序称为词法分析程序或词法分析器或扫描器。[1]
从左至右地对源程序进行扫描,按照语言的词法规则识别各类单词,并产生相应单词的属性字。[1]
词法分析器通常不会关心单词之间的关系(属于语法分析的范畴),举例来说:词法分析器能够将括号识别为单词,但并不保证括号是否匹配。
。语法分析器读取输入字符流、从中识别出语素、最后生成不同类型的单词。其间一旦发现无效单词,便会报错。
词法分析器可以做诸如
1). 去掉注释,自动生成文档(c#中的///注释)
2). 提供错误位置(可以通过记录行号来提供),当字符流变成词法记号流以后,就没有了行的概念
3). 完成预处理,比如宏定义
2.2. 如何描述词素
现在知道了词法分析可以将词素分割开来,那么词素是怎么描述的?或者说,为什么 12、+ 和 34 都是词素,而 1、 2+3 和 4 就不是词素呢?这就需要用到模式了。
模式(pattern)描述了一个词法单元的词素可能具有的形式。
也就是说,我定义了 digit 模式为“由一个或多个数字组成的序列”,和 operator 模式为“单个 + 或 * 字符”,词法分析器就知道 12 是一个词素,而 2+3 则不是词素了。
现在,模式一般都是用正则表达式(regular expression)表示的,这里所谓的正则表达式,与平常所说的正则表达式(例如 System.Text.RegularExpressions.Regex 类)形式完全相同,功能却更有限,它只包含了字符串的匹配能力,而没有分组、引用和替换的能力。简单的举个例子,a+ 这个正则表达式就表示“由一个或多个字符 a 组成的序列”。
2.3. 单词token
这里的单词是一个字符串,是构成源代码的最小单位。从输入字符流中生成单词的过程叫作单词化(Tokenization),在这个过程中,词法分析器还会对单词进行分类。
分析词素的同时还会同时记录下这些词素所在的行、列以便输出错误信息供用户查看,也会同时记录词素的类型。
{
"channel":0,
"charPositionInLine":15,
"inputStream":{"$ref":"$.tokenSource.charStream"},
"line":1,
"startIndex":15,
"stopIndex":15,
"text":"<EOF>",
"tokenIndex":2,
"type":-1
}
]
2.4. Token的类型,根据程序设计语言的特点,单词可以分为五类:关键字、标识符、常量、运算符、界符。以
读者可能对"单词"感到有点疑惑,不明白到底什么才是词法分析中所说的"单词"。试图回答这个问题就必须了解几个基本概念。这里,引入几个程序设计语言相关的名词。
(1)标识符:用户自定义的变量名、函数名等字符串。
(2)关键字:具有特殊含义的标识符。
(3)运算符:例如+、-、*、/ 等。
(4)常量:例如3.24、92等。
(5)界符:具有特殊含义的符号,如分号、括号等。
词法分析的结果是识别出如下的单词符号:
关键字 |
界符 |
标识符 |
运算符 |
常量 |
运算符 |
if |
( |
aa |
&& |
10 |
== |
常量 |
界符 |
标识符 |
运算符 |
常量 |
界符 |
0 |
) |
aa |
= |
100 |
; |
这里,读者只需了解词法分析的任务即可。其算法实现将在第2章中详述
2.5. 词法分析的第一阶段即扫描器
词法分析的第一阶段即扫描器,通常基于有限状态自动机。扫描器能够识别其所能处理的单词中可能包含的所有字符序列(单个这样的字符序列即前面所说的“语素”)。例如“整数”单词可以包含所有数字字符序列。很多情况下,根据第一个非空白字符便可以推导出该单词的类型,于是便可逐个处理之后的字符,直到出现不属于该类型单词字符集中的字符(即最长一致原则
2.6. 词法分析的第二阶段评估器(Evaluator)
,语法分析器需要第二阶段的评估器(Evaluator)。评估器根据语素中的字符序列生成一个“值”,这个“值”和语素的类型便构成了可以送入语法分析器的单词。一些诸如括号的语素并没有“值”,评估器函数便可以什么都不返回。整数、标识符、字符串的评估器则要复杂的多。评估器有时会抑制语素,被抑制的语素(例如空白语素和注释语素)随后不会被送入语法分析器。
2.7. 例如C语言程序段的词法分析结果
例2-1 C语言程序段的词法分析结果见表2-1。
表2-1 词法分析的单词流
源程序字符流 |
词法分析的逻辑结果 |
||||||||||||||||||||||||
int i,j; for (i=1;i<10;i++) j=j+1; |
|
注意,表2-1的单词流并不是词法分析器真正的实际输出结果,只是一种逻辑表示而已。更详细的形式将在后续章节中讨论。根据单词的分类标准,可以将单词作如下归类,见表2-2。
表2-2 例2-1单词流的分类
关 键 字 |
int |
for |
|
|
标识符 |
i |
j |
|
|
运算符 |
= |
++ |
< |
+ |
常量 |
10 |
1 |
|
|
界符 |
, |
; |
( |
) |
这里,读者可能会有两个疑问:
(1)为什么"++"运算符不会分解为两个"+"运算符呢?
(2)为什么将"int i"分解为"int"和"i",而不是"int i"呢?
最长原则
在实际编译器设计中,任何词法分析器都必须满足一个原则,就是在符合词法定义的情况下进行超前搜索识别。例如,当C语言词法分析器读入了一个字符"+"后,由于C语言中存在"++"、"+="运算符,那么,词法分析器会继续读入下一个字符。如果下一个字符是"+"或"="时,词法分析器就将这两个字符作为一个运算符。然而,如果下一个字符不是"+"或"="时,词法分析器就将前一个字符"+"作为一个运算符记录下来后,继续识别下一个单词。
根据这个原则,就可以解释为什么"int"没有被识别为"i"、"n"、"t"了。根据C语言标识符(关键字只是有特殊含义的标识符)定义的规则,标识符必须以字母或下画线开头,后跟字母、数字、下画线的任意组合。因此,当读入"i"后,继续读入"n",由于"in"是合法的标识符,则继续读入"t"。直到读到" "时,发现"int "不满足标识符的定义,则将"int"记录下来即可。
2.8. 最长原则
不过,词法分析器的设计难度很大程度上依赖于程序设计语言本身的规范
在设计一门程序设计语言时,应该尽可能避免关键字非保留字、空格忽略等类似情况的发生,否则将给词法、语法分析造成相当的障碍
2.9. 词法单元的识别
某些状态为接受状态或最终状态,表明已经找到一个词素。
1)关系符转换图
2)保留字和标识符转换图
3)无符号树转换图
4)空白转换图
2.10. 不确定”(Nondeterministic Finite Automata ,NFA
有穷自动机
1)有穷自动机可用作描述在输入串中识别模式的过程,因此也能用作构造扫描程序。当然有穷自动机与正则表达式之间有着很密切的关系
2)有限自动机分成确定的和不确定的两种情况。“不确定”(Nondeterministic Finite Automata ,NFA)的含义是,存在这样的状态,对于某个输入符号,它存在不只一种转换。 确定的和不确定的有限自动机都正好能识别正规集,也就是它们能识别的语言正好是正规式所能表达的语言。
假定一个输入符号(symbol),可以得到2个或者2个以上的可能状态,那么这个finite
automaton就是不确定的,反之就是确定的。例如:
这就是一个不确定的无限自动机,在symbol a输入的时候,无法确定状态应该转向0,还是1
不论是确定的finite automaton还是非确定的finite
automaton,它们都可以精确的描述正规集(regular sets)
我们可以很方便的把正规表达式(regular expressions)转换成为不确定 finite
automaton
下面关于FA和NFA的描述是抄袭AMRJ2010[1]的:
转换的核心是被称为有穷自动机(finite automata)的表示方法。这些自动机在本质上是与状态转换图类似的图,但有如下几点不同:
· 有穷自动机是识别器,它们只能对每个可能的输入串简单的回答“是”或“否”。
2.11. 转换图(transition graph)的表示
我们知道,计算机是无法直接表示一个图,我们应该如何来表示一个转换图?使用表格就是一个最简单的方法,每行表示一个状态,每列表示一个input
symbol,这种表格被叫做 transtion table(转换表)
可以说使用表格是最简单的表示方式,但是我们可以注意到在这个图中状态1和input
symbol a,是没有下一个状态的(空集合),也就是,对于一个大的状态图,我们可能花费大量的空间,而其中空集合会消耗不少空间,但是这种消耗又不是必须的,所以,作为最简单的一种实现方式,却不是最优的
语言(language)被NFA定义成为一个input
string的集合,而这个集合中的元素则是被NFA受接受的所有的字符串(那些可以从开始状态到某接受状态的input
string)
至于存储的方式,可以试试邻接表。注意,使用什么样的数据结构来保存NFA按情况不同而不同,在一些特殊情况下,某些数据结构会变得很方便使用,而换入其他情况,则不可以使用了。
2.12. 词法分析(3)---DFA
1. DFA(Deterministic Finite automaton)
DFA就是确定的有限自动机,因为DFA和NFA关系密切,我们经常需要把他们拿到一起来讲,NFA可以转化成为一个DFA,DFA依然是一个数学model,它和NFA有以下区别
1. 不存在ε-transition,也就是说,不存在ε为input symbol的边
2. 对于move函数,move : (state, symbol) -> S,具体来说就是,一个状态和一个特定的input symbol,不会映射到2个不同的状态。这样的结果是,每个状态,关于每个特定的input symbol,只有一条出边
下图就是一个DFA:
接受语言(a|b)*ab,注意一下,接受语言(a|b)*ab的DFA我们前面见过,就是这张图:
2. DFA的行为
我们用一个算法来模拟DFA的行为
s = s0;
c = nextchar();
while(c != EOF){
s = move(s,c);
c = nextchar();
}
if(s属于F)
return "yes"
else
return "no"
识别词法的过程是用DFA实现的,DFA是类似于下图所表示的东西(其实就是一个状态转换图):
这个DFA只能处理IF、INSERT、INTO三个词,它的运行过程大至描述如下:
1. 声名一个变量(s)用来保存当前的状态。
2. 把开始状态(开始状态就是图中的实心圆点儿)负值给s。
3. 从字符流中读一个字符(c),如果读不出字符就终止算法。
4. s的边上有字符,就代表s输入这个字符之后可以沿着这个边走到下一个状态。此时看一下s输入c可以到哪个新状态里去。如果不能到到达一个新状态,则说明这个DFA不能解析这个字符流(到此终止算法),否则s的值变成新的状态。
5. 看一下s是否为终止状态(也叫接受状态,图中用带白边的圆点儿表示),如果是终止状态,则解析到一个字符,然后回到第2步,如果不是终止状态,则回到第3步。
差不多就是这样的,实际情况比上面所说的要稍复杂一点(比如冲突解决、匹配原则),后面会详细讲。
这个DFA只能识别三个单词,实际的编译器中肯定是要能识别一个语言中所有的词素,那样一个DFA是很庞大的,如何去来概造这个完整的DFA也是后面要讲的内容
2.13. 为什么要NFA转DFA
到此正则表达式转NFA的内容就全讲完了。虽然NFA也可以运行,并且也可以用来识别语言的词素,但其运行过程要比DFA复杂得多,而且除非我们可以并发的运行NFA的每个分支,否则NFA的执行速度绝对分比NFA的执行速度要慢。我们现在拥有的计算机一般都只是PC机,还没有那么强的并发能力,所以NFA转DFA就成了词法分析的一个必要的程。
另外,某些正则引擎用NFA来运行,这是基于引擎使用的实际情况来考虑的。因为NFA转DFA也是要时间的,并且如果引擎经常使用在高并发能力的计算机上,那么直接用NFA来运行还会快一些。而编译器通常不这么做是因为编译器在发布时只发布DFA就行了,NFA转DFA的过程最终用户并不会接触到。这也是词法分析程序与正则引擎的不同之处。
下一节来讲一下NFA转DFA的方法。
2.14. 则表达式转NFA
正则表达式是什么?这个问题不在这里详述。上网搜一下,很快就能了解基本概念。有一本书《精通正则表达式》,这本书第一章(20多页)看完就会写基本的正则表达式了。其电子版在网上有下载。
直接做一个可以识别一个语言所有词素的DFA是非常困难的,而且即使做出来,日后的修改同样非常麻烦。而用正则表达式(正则文法)来描述词素就简单得多,同时日后这个语言要修改或增加新的词素都很简单。所以现在的词法分析器的构造方式都是先用一种基于正则文法的语言来描述所有词素,再把这一描述转换成DFA。正则文法转DFA的常规方法是需要一个中间过程的,即先把正则文法的描述转成NFA,而从NFA到DFA的转换方法是存在的。
2.15. 正则表达式如何转换为NFA呢?有几个公式(MLS2007[1]):
公式1:如果一个正则表达式只有一个字符'a',那么NFA如下图:
即:从开始状态,输入一个字符a,就到达了接受状态。
公式2:如果一个正则表达式是两个表达式连成的,如ab,那么NFA如下图:
即:从开始状态,输入a,到达状态1,再输入b到达接受状态。这个公式相当于把两个“公式1”前后连接而成的。
公式3:如果一个正则表达式是这样的:a|b,即二选一的情况,那么NFA如下图:
图中我有几条边是没有画输入的,那么就是Ɛ,即:空输入或无输入,以后为了画图方便,Ɛ输入就不画在图中了。
这个图描述的就是:从开始状态,可以向上走1,也可以向下走3,如果走1,那输入a就走到2,如果走3,那么输入b就走到4,2和4都有一个空输出到接受状态。
这个图相当于把两个“公式1”的并排放到一起,前面接一个状态做为开始,后面接一个状态做为结束。
公式4:如果一个正则表达式是Kleen必包:a*,那么其对应的NFA如图:
这个图稍微解释一下:从开始有两条空输入边,一条直接到接受状态,这表示一个a都不接受,另一个空输入边到1,1只有一个出口就是输入一个a到2,2状态可以直接到达接受状态,也可以回到1,这样就可以达到接受任意多个a的情况。
有了上面四个公式,就可以达到匹配任何字符的目的了(还不能匹配位置,不过对于编译器的词法分析是不需要匹配位置的),举个例子a*|bc就可以用“公式4”把a*的图画出来,用“公式2”把bc的图画出来,再用“公式3”把前两个图连接上就行了,如图:
上面四个公式上最基本的公式。大多数正则表达式也会识别其它的结构,如:a?、a+,其实这也可以用以上公式来做:a?可以等价于a|Ɛ(其实这个只要把a表示的NFA从开始状态拉一个空输入的边到接受状态就可以了,不需要使用“公式2”的,“公式2”主要是使用于两个正则表达式之前的或关系,如果两个表达式有一个为空,可以简便一点处理),a+等价于aa*,这样我们还是可以用基本公式来处理。
基本公式有了之后,还需要处理一些括号,下面分别讲一下:
方括号[]:代表字符组,就是指方括号中的字符任选其一的意思。例如:[abc]就是指匹配a或匹配b或匹配c,即与a|b|c等价。特殊情况是当方括号内的第一个字符是^时,表示排除形字符组,就是指广括号中,除了第一个^之外的其它字符都不匹配,例如[^abc]就是指不能匹配a,也不能匹配b,也不能匹配c。另外,在字符组中可以使用连字符(-),例如[a-d]和[abcd]是等价的。
方括号转NFA的一个比较简单的做法是把整个字符组做为一条边的输入,这样做的话,那么表示NFA的某状态的输入就不是单个字符,而是一个字符串,只要当前字符是(或者不是,当是排除形字符组时)这个字符串中的字符即可。这样的处理方式就可以套用前面的“公式1”了。
对于连字符(-)的处理一般有两种方法。如果语言的字母表比较小(比如ASCII),那么只要把连字符展开就可以了,例如:[a-z]就直接用[abcdefghijklmnopqrstuvwxyz]来替换。如果语言的字母表很大(比如Unicode),那么就不展开,如果这样展开,那这一个字符串就要占用非常大的内存,这时的做法是把连字符直接放到输入里,不在转换与此同时文法的时候处理,而在运行的时候用“大于等于”和“小于等于”来判断。
小括号():代表在正则表达式中限定一个范围,也就是改变有限级的做用。例如:a*|bc和(a*|b)c这两个表达式,我们知道“合取”的有限级是高于“析取”的(这里用“合取”和“析取”不太标准,不过因为我想到如果用“与”和“或”仍然不太标准,所以我选择用两个稍生僻点的名词,可以多吸引一下读者的眼球,或许可以因此减少对这里的不准确的描述的误解),所以a*|bc对应的NFA图是这样的:
而(a*|b)c改变了优先级,此时要先做“析取”再做合取,其对应的NFA图是这样的:
对于小括号的处理方式是先把括号内的部分做为一个整体再处理。例如:(a*|b)c,先把a*|b做为一个整体A,那么就变成了(A)c此时小括号就没用了,可以去掉,就变成了Ac,这样就可以套用“公式2”了。之后再处理a*|b,此时没有括号,也可以套用基本公式(如果有嵌套的小括号,则前面的办法,把括号内的部分做为一个整体)。之后再把转换完a*|b的NFA放到之前A在图中的位置就可以了。
花括号{}:用来引用前面已经定义过的正则表达式(我在写代码的时候用了尖括号<>,flex用的是花括号,我打算以后重写的时候用花括号,因为花括号好看一点)。正则文法的准确定义我不在这里详述,用我的话简单说来就是一系列的正则表达式(每个表达式有一个名字和一个定义),后面的表达式不但可以包含字母表中的内容,还可以包含前面已经定义过的表达式。这里我们就用花括号来引用前面已经定义过的正则表达式的名字。
对于花括号的处理比较简单:我们只要把花括号部分用前面的定义来替换就行了。实际写代码的时候我们可能在转换NFA的时候把前面已经转换完成的NFA图拿过来用就行了,而不需要去替换其定义。
2.16. 构造词法分析器了。大致的流程如下:
图 3 构造词法分析器
Regexpre >>nfa>>dfa>>simple dfa>>convert table>>dfa simulaer>>tokens..
从上图来看,定义了模式的正则表达式,经过 NFA 转换、DFA 转换和 DFA 化简,得到了一张转换表。这张转换表再加上一个固定的 DFA 模拟器,就组成了词法分析器。它不断的从输入缓冲区中读取字符,利用自动机来识别词素并输出。可以说,词法分析的精华就是如何得到这张转换表
2.17. 常用的token scanner
Hb 使用antlr...mysql 使用的customez..,但是语法分析却用了yacc
2.18. 词法分析器也能检测到源代码里边的一些错误
词法错误:
词法分析器是很难(有些错误还是可以检测)检测错误的,因为词法分析器的目的是产生词法记号流,它没有能力去分析程序结构,因此无法检测到和程序结构有关的错误
从词法分析阶段中,词法分析器也能检测到源代码里边的一些错误。例如在Zend引擎的词法分析阶段就有这样一段代码:
zend_error(E_COMPILE_WARNING, “Unterminated comment starting line %d”, CG(zend_lineno));
当检测到/*开头,但是没有*/结尾时,Zend引擎会抛出一个Waring提示
但是并不影响接下来的词法解析,词法分析阶段一般都不会造成严重的解析错误,因为词法分析阶段的职责就是识别出Token序列而已,它并不需要知道Token跟Token之间是否具备什么联系(那个应该是语法分析阶段的职责)。在Zend引擎的词法分析器中也会抛出致命的解析错误而终止词法分析阶段,如下代码:
zend_error_noreturn(E_COMPILE_ERROR, “Could not convert the script from the detected “
“encoding \”%s\” to a compatible encoding”, zend_multibyte_get_encoding_name(LANG_SCNG(script_encoding)));
这个解析错误是因为从输入流里边检测到的代码的编码不合法,显然,这里是应该终止掉整个解析过程的。
Zend引擎的词法分析器re2c来生成,词法分析的阶段会涉及到各个状态,其变量命名均为yy开头(下文会说明)。
2.19. 参考
2.1.1 词法分析的任务 - 51CTO.COM.html
【编译原理】第三章 词法分析 - 小田的专栏 - 博客园.html
C# 词法分析器(一)词法分析介绍 update 2014.1.8 - CYJB - 博客园.html
2、JavaScript高级之词法分析 - Javascript教程_JS教程_技术文章 - 红黑联盟.html
3、词法分析(NFA与DFA) - woaidongmao - C++博客.html
4、一个编译器的实现(02)——词法分析(1.正则转NFA)-naturemickey-ChinaUnix博客.html