首页 > 其他分享 >自己动手写编译器:词法解析的系统化研究

自己动手写编译器:词法解析的系统化研究

时间:2023-06-14 11:33:02浏览次数:41  
标签:字符 词法 字符串 编译器 集合 我们 表达式 系统化


在前面章节中,我们千辛万苦的做了一个可以将部分c语言代码进行解析并编译成中间语言的微型编译器,通过实践我们对编译技术的整体架构和实现原理有了一定的感性认识,实现了“没吃过猪肉但见过猪跑”,从本节开始,我们正式进入“吃猪肉”的过程,我们将非常系统的去研究编译原理各部分理论和算法,从这节开始,我们系统化的去剖析词法解析的算法流程。

前面我们实现过词法解析,他的大概流程是,将文本读入,然后从文本中取出一个单位字符串,分析它的成分,到底是变量,数字,还是关键字,然后给他创建一个抽象描述对象叫token,于是源码字符串文本就变成了token的集合,而语法分析的主要任务是通过分析token集合所描述的含义来建立抽象语法树,并最终将代码转义成机器码或是中间代码。

但是我们以前实现的词法解析比较“低层次”,也就是我们写死了词法解析的规则。也就是我们的词法解析只能识别特定语言的字符串,例如我们原来实现的词法解析只能识别部分c语言代码。事实上词法解析还有更高层次,那就是它识别的不是具体的语言,而是给定的“规则”,这种词法解析能力也叫词法解析生成器。如今编译器开发早已经形成了一个完善的工具链,词法解析器不仅能用于解析c语言代码,也能用来解析python, java等,只要你给他既定的词法规则,他就能生成我们前面实现的,能执行针对特定语言的词法解析代码,因此它也成为词法解析引擎。

下面我们需要了解三个比较抽象的概念:
token: 他是程序意义上的一个抽象对象,他包含一个数值,用来描述一个字符串单元所属类别,同时还包含他所针对的字符串,同时还包含一些其他信息,例如它所描述字符串所在的行号,文件等,这个概念我们在前面实践中早有体会。

pattern: 他用来描述特定token类别所要求的字符组合规律,例如ID,他对应的pattern就是“由字符和数字组成,同时字符作为开头的字符集合",pattern是词法解析的核心,后面我们会看到有很多复杂且精妙的算法和数据结构来实现pattern对应的识别能力。显然所有能满足一个给定组合规律的字符串都属于一个token类别

lexeme,这个概念我们前面也了解过,他就是一系列字符组成的字符串,而且这些字符的组合满足pattern描述的要求。我们看一个具体例子:

print("Total = ", score)

在这条语句中 print, score 属于lexeme,他们对应的token是ID, 同时Total= 对应的token是literal,也就是字符串。当特定字符组成的字符串满足特定pattern,或者说是组合规律时,词法解析器就会创建一个对应的token对象来对这些字符串集合进行抽象意义上的描述。但是token不仅仅要描述抽象上的性质,还要包含具体的属性,例如数字0,1都属于类别NUM,但是对编译器而言,知道类别NUM对应的数值至关重要,因此token除了描述类别外,还需要描述类别的具体对象,如果了解面向对象的编程,那么token就包含了类的定义和类的具体实例。

在进行词法解析时,我们还需要执行一定的缓存。前面实践我们也看到,在识别字符串属于那种类别时,我们通常需要读取下一个字符来决定,例如当前读到的符号是"=“那么我们需要看下一个符号,如果下一个符号是”=",那意味着两个‘=’符号必须合并在一起解读。通常我们会把一个磁盘区块的内容读入内存,通常是4096个字节,然后用两个指针来读取字符,第一个指针叫lexmeBegin,指向当前字符串的开头,另一个指针forward从lexemeBegin位置开始不断后移,直到两个指针中间字符组合满足给定匹配规则,也就是patter为止,然后下一次读取时lexemeBegin指向当前forward所在位置的下一个字符:

自己动手写编译器:词法解析的系统化研究_正则表达式


其中eof是一个特定符号,它表示文本结束,后面不再存有有效字符。这点我们在前面实现中也体会过。

下面我们要探寻一些较为抽象的概念,例如”字符“,”字符串“ 和 ”语言“。首先”语言“是对满足特定条件元素集合的描述,例如所有满足c语言语法规则的字符串文本都叫c“语言”,显然这个集合的元素个数为无限,因为我们可以用c语言写出不同样的程序。对于二进制数而言,它的字符只有两个,分别为0和1,那么由0和1排列组合而成的所有字符串所形成的集合也是一种“语言”。而一个特定的字符集合就叫”字符串“。

这里有一个递进的层次关系,最底层的就是”字符“,例如英语中的26个字母。接着是字符串,也就是字符按照某种规则组成的集合,例如"hello",在英语中就是单词。最后就是语言,也就是字符串按照某种指定规则形成的集合,例如英语。我们这里谈到“某种规则”,我们需要使用一些特定的数学符号来对其进行描述,对人类语言而言,目前我们找不到数学或逻辑的方式来描述其组成规则,但对编程语言而言,我们不难找到。

首先我们规定字符集合的几种运算,假设我们有两个字符集合L={a,b…,z, A,B…Z}, D={0,1,2…9},针对这两个字符集合,我们定义如下运算或操作:
1, L ∪ D 表示在两个集合的并,于是结果就是{a, … z, A…Z, 0… 9},
2,LD,表示在L中选择一个字符,然后D中选择一个字符,两者前后连接,由于L中有52个字符,D中有10个字符,因此这种连接结果总共有520种,于是LD集合包含520个元素
3,自己动手写编译器:词法解析的系统化研究_字符串_02 表示从L中任意选择4个字符形成字符串,因此该集合的元素个数就是自己动手写编译器:词法解析的系统化研究_编译原理_03
4,自己动手写编译器:词法解析的系统化研究_词法解析_04 表示从L中选取0个或任意多个字符进行组合,如果是0个的话,对应的结果就是空集。
5,自己动手写编译器:词法解析的系统化研究_字符串_05表示从L中选择1个或任意多个字符进行组合,因此它是情况4中排除掉空集的结果

有了上面描述的运算后,我们就能描述字符的特定组合规则,而这种描述方式是所谓的“正则表达式”。 我们利用上面规则就能描述C语言中变量名的组合方式:
letter_自己动手写编译器:词法解析的系统化研究_正则表达式_06
它表示以字符或下划线开始,后面跟着任意多个字符,下划线或数字的组合。其中“|”表示“或”,下面我们需要看看有点烧脑的逻辑描述,我们看到给定正则表达式,例如上面那个,那么他就定义了一种字符串集合,所有满足正则表达式规定的字符串的集合就叫做对应正则表达式的语言,如果我们用r来表示给定正则表达式,用L®表示所有满足r的字符串的集合,也就是L®表示表达式r生成的语言,其实我们也可以把L®看成一种函数映射,给定一个表达式后,它“映射”出一个字符串集合,这里我们给这个“映射”增加几个越苏条件:
1,自己动手写编译器:词法解析的系统化研究_编译原理_07表示一个空表达式,那么L(自己动手写编译器:词法解析的系统化研究_编译原理_07)映射出一个空的字符串集合
2,我们用自己动手写编译器:词法解析的系统化研究_词法解析_09 表示一个字符集合,例如26个字母的集合就是自己动手写编译器:词法解析的系统化研究_词法解析_09={a,b…z},如果a是集合中的一个字符,那么它本身也自然是一个表达式,同时有L(a) = {a},也就是对于映射L,如果输入是单个字符,那么他生成的集合仅仅包含这个字符。
3,如果r 和 s 分别是两个正则表达式,®|(s)是两个表达式的“并”组合,那么由®|(s)表达式生成的语言对应r和s各自生成语言的并, 也就是L(®|(s)) = 自己动手写编译器:词法解析的系统化研究_字符串_11
4, r 和 s是两个正则表达式,表达式®(s)对应的字符串集合或语言就是 L(®(s)) = L®L(s),L®L(s)它表示字符串的前缀部分来着L®,后缀部分来自L(s)。

5, r是一个表达式,自己动手写编译器:词法解析的系统化研究_词法解析_12也是一个表达式,他对应的语言或字符串集合就是自己动手写编译器:词法解析的系统化研究_正则表达式_13

6,r 是一个表达式, ( r )同样也是一个表达式,同时L( (r ) ) = L( r ) ,也就是我们给一个表达式加上括号后所得的新表达式,他所形成的语言给原来一样。
7,如同四折运算,括号的作用表示优先级,同时在运算中乘法和除法的优先级高于加法和减法,在正则表达式中不同操作符也一样有优先级,其中*操作的优先级最高,而且他总是作用于左边的表达式,例如r*s 等价于(r*)s
8,连接操作符具有第二优先级,| 具有最低优先级,也就是rs|z,表示(rs)|z

上面这组规则比较抽象不好理解,应该是一个“劝退点”,不过后面我们通过代码实现就能比较容易的理解这些抽象规则。我们看一个例子:
假设字符集包含两个字符自己动手写编译器:词法解析的系统化研究_词法解析_09={a,b}, 那么表达式 a | b = {a, b}, (a|b)(a|b)={aa, ab, ba, bb},自己动手写编译器:词法解析的系统化研究_词法_15 = {自己动手写编译器:词法解析的系统化研究_编译原理_07, a, aa, aaa, …}。(a|b)* 表示所有包含0个或多个a或b组成的字符串,也就是{自己动手写编译器:词法解析的系统化研究_编译原理_07, a, b, ab, aab, aaab, ba, bba, bbba, …},a|a*b表示集合{a, b, ab, aab, aaab…}。

如果给定两个正则表达式,r, s,倘若他们各自生成的字符串集合完全相同,那么我们就定义 r = s,例如(a|b) = (b|a),正则表达式在运算符 | , * 下有特定的代数性质:
1,交换律: r | s = s | r ,
2,结合律:r | (s | t) = (r | s) | t
3,分配律:r ( s | t) = rs | rt ,(r|s) | t = rt | st
4,自己动手写编译器:词法解析的系统化研究_编译原理_07r = r自己动手写编译器:词法解析的系统化研究_编译原理_07 = r , 这里自己动手写编译器:词法解析的系统化研究_编译原理_07,类似于乘法中的数字1
5,自己动手写编译器:词法解析的系统化研究_词法解析_12 = 自己动手写编译器:词法解析的系统化研究_编译原理_22
6, 自己动手写编译器:词法解析的系统化研究_词法解析_12自己动手写编译器:词法解析的系统化研究_词法解析_24 = 自己动手写编译器:词法解析的系统化研究_词法解析_12

为了简化对正则表达式的描述,我们可以给表达式“起名字”,后面我们在描述中就可以使用名字来指代给定表达式,这种方法也叫正则定义,我们用自己动手写编译器:词法解析的系统化研究_词法解析_09表示字符集,那么正则定义具有如下形式:
自己动手写编译器:词法解析的系统化研究_词法解析_27 -> 自己动手写编译器:词法解析的系统化研究_正则表达式_28
自己动手写编译器:词法解析的系统化研究_编译原理_29 -> 自己动手写编译器:词法解析的系统化研究_编译原理_30

自己动手写编译器:词法解析的系统化研究_词法解析_31 -> 自己动手写编译器:词法解析的系统化研究_词法_32

其中自己动手写编译器:词法解析的系统化研究_编译原理_33 是不在集合自己动手写编译器:词法解析的系统化研究_词法解析_09中的符号,同时自己动手写编译器:词法解析的系统化研究_编译原理_33 != 自己动手写编译器:词法解析的系统化研究_词法_36 如果i != j。并且自己动手写编译器:词法解析的系统化研究_正则表达式_28是基于字符集合自己动手写编译器:词法解析的系统化研究_词法解析_09上的正则表达式。通过这种定义方式,我们可以有效简化嵌套表达式的描述,例子如下:

letter_ -> A | B … | Z | a | b …|z _
digit -> 0 | 1 | 2 | …| 9
id -> letter_ (letter_ | digit) *

对于整数,浮点数如:5280, 0.01234, 6.336E4, 1.89E-4,等,他们对应的表达式定义如下:
digit -> 0 | 1 … | 9
digits -> digit digit*
optionalFraction -> . digits | 自己动手写编译器:词法解析的系统化研究_编译原理_07
optionalExponent -> (E (+ | - | 自己动手写编译器:词法解析的系统化研究_编译原理_07) digits) | 自己动手写编译器:词法解析的系统化研究_编译原理_07
number -> digits optionalFraction optionalExponent

正则表达式的操作符除了上面描述的*, |,之外,还有一些扩展,分别是自己动手写编译器:词法解析的系统化研究_词法_42表示r至少要重复一次,同时有L(自己动手写编译器:词法解析的系统化研究_词法_42) = 自己动手写编译器:词法解析的系统化研究_编译原理_44,还有操作符?,他表示给定表达式出现0次或1次,也就是r? = r | 自己动手写编译器:词法解析的系统化研究_编译原理_07, 同时有L(r?) = 自己动手写编译器:词法解析的系统化研究_词法_46,同时操作符*, ?, + 拥有相同的优先级。同时我们还需要知道有一种表达式叫字符集类,也就是r = 自己动手写编译器:词法解析的系统化研究_编译原理_47,其中自己动手写编译器:词法解析的系统化研究_字符串_48是属于集合自己动手写编译器:词法解析的系统化研究_词法解析_09中的字符,这种表达式也可以写为[自己动手写编译器:词法解析的系统化研究_编译原理_50], 同时[a-z]表示[a|b|c|…|z],于是上面的例子我们又可以进一步简化:
letter_ -> [a-zA-z]
digit-> [0-9]
id ->letter_自己动手写编译器:词法解析的系统化研究_词法解析_51

本节我们描述的理论不好理解,也比较枯燥,因此我们大概了解一下就行,后面我们会通过代码以更加形象和具体的方式来体会这里描述的内容,更多内容请在B站搜索coding迪斯尼。


标签:字符,词法,字符串,编译器,集合,我们,表达式,系统化
From: https://blog.51cto.com/u_16160261/6476550

相关文章

  • java开发C编译器:把函数调用编译成字节码
    本节,我们研究如何把函数声明和函数调用转换成可执行的java字节码,在完成本节代码后,我们的编译器能把下面代码编译成可被java虚拟机执行的字节码,示例代码如下:voidf(){printf("executefunctionf()");}voidmain(){f();}假设java一个类含有如下方法:publicfloatco......
  • java开发编译器:LR 状态机的缺陷与改进
    前两节我们构造的状态机有些缺陷,当我们进入某个状态节点时,根据该节点的特性,我们需要产生一些动作,根据上两节的有限状态机图,当我们进入节点5,我们发现,符号”.”为位于表达式的最右边,在.后面不再有其他非终结符或终结符,进入这样的节点时,我们要根据表达式做一次reduce操作,例如在节点5......
  • java开发C编译器:结构体的解析和执行
    更详细的讲解和代码调试演示过程,请参看视频用java开发C语言编译器结构体是C语言中,最为复杂的原生数据结构,它把多种原生结构结合在一起,形成一个有特点含义的数据结构,要实现一个完整的C语言编译器或解释器,就必须要拥有对结构体的解析能力,本节,我们在当前解释器的基础上,增加结构体的解......
  • java开发C语言编译器:JVM 的基本操作指令介绍及其程序运行原理
    更详细的讲解和代码调试演示过程,请参看视频用java开发C语言编译器更详细的讲解和代码调试演示过程,请参看视频如何进入google,算法面试技能全面提升指南如果你对机器学习感兴趣,请参看一下链接:机器学习:神经网络导论更详细的讲解和代码调试演示过程,请参看视频LinuxkernelHacker,......
  • 编译原理动手实操,用java实现一个简易编译器-语法解析
    语法和解析树:举个例子看看,语法解析的过程。句子:“我看到刘德华唱歌”。在计算机里,怎么用程序解析它呢。从语法上看,句子的组成是由主语,动词,和谓语从句组成,主语是“我”,动词是“看见”,谓语从句是”刘德华唱歌“。因此一个句子可以分解成主语+动词+谓语从句:句子-->主语+动词+谓语......
  • java开发C语言编译器: return 语句的解释和执行
    在C语言程序中,很多函数并不是执行全部语句后,才从最底部返回的,大多数情况下,当某些条件成立时就可以通过return语句立即返回,而无需执行接下来的代码,本节,我们继续增强java开发的C语言解释器功能,使其能够处理return语句,完成本节代码后,我们的C语言解释器能够正常解析和执行下面的代码:in......
  • java实现C语言编译器:实现有参数的函数调用
    上一节,我们实现了没有参数传递的函数调用,本节,我们看看如何实现有参数传递的函数调用。有参数的函数调用要比无参数的函数调用复杂的多,一个难题在于,我们需要确定参数变量的作用域,例如下面的代码:inta;voidf(inta,intb){intc;c=a+b;}在代码里,有两个同名变量都......
  • Latex编译器推荐(面向初学者或者懒得折腾的朋友,主要针对windows用户)
    原文链接:https://blog.sciencenet.cn/blog-478347-1215384.html大家平时用的最多的排版工具想必就是Microsort的Word或者WPS了,所见即所得,Latex是另外一种排版工具,需要编译才可以生成pdf。相信大家在投稿的时候,会发现很多杂志都提供的textemplate。至于Latex好还是word好,这个已......
  • 简单编译器
    目录0x01背景0x02SML语法0x03应用源码:编译过程:sml.txt文件:执行0x04SML_C实现sml_compiler.hsml_compiler.c0x05总结0x01背景《C语言大学教程-第八版》(《CHowtoProgram》)391页,第十二章作业,专题:创建自己的编译器在练习题7.27至练习题7.29中,我们介绍了Simpletron机器......
  • 当GaussDB遇上了毕昇编译器
    摘要:当应用软件及硬件确定后,编译器对应用的自动优化将成为应用性能的关键。从应用优化说起一个应用的优化通常有架构级优化、模块级优化和函数级优化,高性能作为云数据库GaussDB主打特性之一,其在这几方面都进行了大量的优化,也有很强的性能表现。如何进一步提升性能,是否还有其他方......