编译原理——词法分析

文章目录


词法分析:从基础到自动构造

在编译原理的世界里,词法分析作为编译的第一个阶段,承担着将输入的源程序分割成一个个单词单元的重要任务。它就像是一位勤劳的 "分拣员",把杂乱无章的字符流整理成有意义的单词序列,为后续的语法分析等工作奠定基础。

当源程序的字符流输入到词法分析器中,词法分析器会按照预先设定的规则,对字符逐一进行检查和识别。比如,对于一段简单的 C 语言代码 "int num = 10;",词法分析器会将其分割为 "int"(关键字)、"num"(标识符)、"="(运算符)、"10"(常量)和 ";"(界符)这些单词单元。这种将无结构的字符流转化为有结构的单词序列的操作,就如同把一堆杂乱的零件分类整理成一个个可识别的部件,为后续的语法分析提供了有序且易于处理的输入。

一、词法分析程序的设计

很抱歉给你带来了困扰,可能是出现了一些误解,并非有意将内容改为英文。我将立刻把这部分内容重新翻译回中文,并且会依据你最初对内容丰富、添加表格的要求,进一步优化完善。

一、词法分析程序的设计

设计一个词法分析程序,就如同构建一个复杂且精密的机械装置。明确其输入与输出,是构建这个装置的基石。输入的源程序字符流,可被视作一条源源不断的原材料输送带。每个字符就像一个个无序的小零件,依次流入词法分析这个庞大而忙碌的"生产车间"。而输出则是一个个被精准识别的单词,这些单词就像是经过精心预处理的标准化组件,具备特定功能和确切含义。它们整齐排列,急切地等待进入下一个关键阶段------语法分析。

在整个设计过程中,字符扫描方式的选择起着决定性作用。业界通常采用从左至右逐个字符扫描的经典策略。这类似于我们自幼养成的从左至右阅读单词和句子的习惯,既自然又符合大多数人的思维模式。实际上,除了从左至右逐个字符扫描的方法外,还有其他扫描方式,每种方式都有其优缺点,具体如表1:字符扫描方式对比 所示。

扫描方式 优点 缺点 适用场景
从左至右逐个字符扫描 简单直观、易于实现,符合人类阅读习惯 对某些复杂语言结构的识别效率较低 大多数常见编程语言
从右至左逐个字符扫描 在处理诸如后缀表达式等特定语言结构时具有优势 不符合常规阅读习惯,实现起来相对复杂 特定领域语言,如逆波兰表示法语言
块扫描(例如按固定长度的单词块扫描) 当字符流规律性较强时,扫描速度快 灵活性差,难以适应单词长度多变的语言 一些标记语言,如某些简单的配置文件语言

如何准确识别不同类型的单词,如关键字、标识符、运算符和常量等,无疑是词法分析程序设计的核心与关键任务。一般来说,会巧妙运用状态转换的思想。通过状态之间的动态转移,能够准确确定当前所识别单词的类型。不同类型单词的识别复杂度也有所不同,详见表2:单词类型识别复杂度分析 。

单词类型 识别复杂度 主要影响因素
关键字 它们来自预定义的固定集合,精确匹配即可
标识符 需要根据字符组合规则进行判断,状态转移相对复杂
运算符 特定符号对应特定运算符,易于识别
常量 中(数字常量低,字符串常量相对较高) 数字常量格式相对固定,而字符串常量需要处理引号及内部字符

以关键字"int"的识别为例,当从状态S0扫描到字符'i'时,会进入专门用于识别关键字"int"的中间状态S3。如果紧接着扫描到字符'n',就会进入状态S4,再扫描到字符't'时,关键字"int"就被成功识别,进入最终状态S5。这种状态转移的差异,生动地体现了不同类型单词识别过程的独特性。不同编程语言在各类单词的定义和识别方式上也存在显著差异。以关键字为例,我们进一步扩充表3:常见编程语言关键字对比 。

编程语言 部分关键字示例 关键字识别特点 关键字功能简述 关键字数量(大致范围) 与新特性相关的关键字(示例)
C int、if、while、return 关键字集合相对固定,词法分析器通过精确匹配进行识别 用于定义变量类型、流程控制、函数返回等 约32个 _Bool(C99中引入,用于定义布尔类型)
Python if、else、for、def、class 区分大小写,严格匹配预定义的关键字集合 实现条件判断、循环控制、函数定义、类定义等 约35个 async、await(Python 3.5中引入,用于异步编程)
Java class、public、private、static、void 关键字具有明确的语法和语义功能,词法分析时精确匹配 构建类、设置访问权限、定义静态成员、声明方法返回类型等 约50个 var(Java 10中引入,用于局部变量类型推断)
JavaScript var、function、let、const、async 关键字集合可能随版本更新而变化,需要准确识别 变量声明、函数定义、块作用域变量声明、常量声明、异步函数定义等 约49个 yield(用于生成器函数)
C++ int、double、if、else、while、class、template 关键字集合丰富且复杂,采用精确匹配进行识别 数据类型定义、流程控制、类定义、模板编程等 约96个 concept(C++20中引入,用于概念定义,增强模板编程)

为了更清晰、全面地阐述不同单词类型在一般情况下的识别规则,我们参考表4:通用单词类型识别规则 。

单词类型 示例 识别规则 在程序中的作用 与其他单词类型的关联(示例)
关键字 int、while等 预定义的固定字符串集合,词法分析器通过精确匹配进行识别 作为编程语言的语法基石,引导程序的结构和逻辑走向。例如,"if"用于条件判断的开头 与标识符结合。例如,在"int num;"中,"int"定义了"num"的类型
标识符 num、count等 以字母开头,后跟字母或数字的字符串,如上述通过状态转移进行识别 为变量、函数、类等程序元素命名,便于在程序中引用和操作。例如,"count"可作为计数变量名 用作函数参数、返回值等,并与运算符和常量一起参与表达式运算
运算符 +、-、*、/等 根据运算符的特定符号进行识别 执行各种数学、逻辑或其他操作。例如,"+"用于加法运算 连接标识符和常量以构建表达式,如"num + 10"
常量 10、3.14、"hello"等 数字常量根据数字格式进行识别,字符串常量识别为用引号括起来的字符序列 表示固定值,为程序提供数据支持。例如,"3.14"可用于数学计算中的圆周率近似值 作为表达式中的操作数,与运算符和标识符协同工作

我将把内容完整翻译成中文,同时进一步优化表述,结合具体案例让阐述更清晰。

二、PL/0编译程序中词法分析程序的设计与实现

PL/0是一种简单且易于理解的编程语言,其编译程序里的词法分析程序设计独具特色。在实现过程中,必须依据PL/0语言的语法规则构建词法分析逻辑。

1. 语法特定考量

  • 关键字识别:PL/0有一组预先定义好的关键字,比如"begin""end""if""then""while""do"等。这些关键字在定义PL/0的控制流和程序结构方面起着关键作用。例如,"if - then"结构用于条件执行。词法分析程序需要极为准确地识别这些关键字。它通常通过维护一个关键字表来实现这一点。在扫描源代码时,对于每个类似单词的标记,它会检查该标记是否与关键字表中的任何条目匹配。如果匹配,就赋予相应的关键字类型。
  • 标识符命名规则:PL/0中的标识符遵循特定规则。它们必须以字母开头,后面可以跟一系列字母和数字。例如,"count""sumValue""userInput"都是有效的标识符。为了处理标识符识别,词法分析程序采用基于状态的方法。当它首次遇到一个字母时,就进入"标识符构建"状态。只要后续字符是字母或数字,它就保持在这个状态。一旦遇到非字母数字字符,该标识符就被视为完成,并将其类型标记为标识符。
  • 常量表示:PL/0支持整数常量和字符串常量。整数常量就是简单的数字序列,像"10""256"或" - 15"。字符串常量用单引号括起来,例如'Hello, PL/0!' 。在处理整数常量时,词法分析程序在扫描时只需累加数字。对于字符串常量,当遇到单引号时,它就进入"字符串起始"状态,然后持续收集字符,直到遇到结束的单引号。

2. 通过状态转移表运用有限状态自动机

  • 状态转移表构建 :状态转移表是PL/0词法分析程序设计中的一个基础工具。该表由代表不同状态的行和代表可能输入字符的列组成。例如,在初始状态(我们称其为S0)下,如果遇到字母字符,程序就转移到"标识符构建"状态(比如S1)。在S1状态下,如果扫描到另一个字母或数字,它就保持在S1。但如果检测到非字母数字字符,它就转移到"标识符后"状态(S2)。
    • 对于关键字识别,当处于初始状态S0且扫描到关键字起始字符(比如"begin"的'b')时,它可能转移到特定于该关键字的中间状态。例如,从S0开始,扫描到'b'后,转移到S3。接着,如果扫描到下一个字符'e',就移动到S4,以此类推,直到在该关键字的最终状态中识别出整个"begin"。
    • 在处理常量时,从S0开始,如果扫描到数字用于整数常量,它就进入"整数构建"状态(S5)。在S5中,后续数字使它保持在同一状态,当遇到非数字字符时,整数常量就完成了。对于字符串常量,从S0开始,当扫描到单引号时,它进入"字符串起始"状态(S6),并在一系列状态中持续收集字符,直到找到结束的单引号。
  • 高效词法分析:通过基于有限状态自动机原理使用这个状态转移表,PL/0词法分析程序能够高效地逐个字符处理源代码。它能快速确定字符流中每个标记的类型。例如,考虑PL/0源代码"begin if x > 10 then y := 20; end"。词法分析程序从S0开始,扫描到"begin",识别出它是一个关键字,接着移动到"if",这也是一个关键字。当扫描到"x"时,它进入标识符构建状态,当遇到">"符号时,识别出它是一个运算符。这个过程持续进行,将整个字符流转换为符合PL/0语言规则的标记序列。

3. 示例代码片段(用于说明的伪代码)

下面是一个简单的伪代码示例,展示如何为PL/0实现基于状态的词法分析:

复制代码
currentState = S0
inputIndex = 0
inputString = "begin if x > 10 then y := 20; end"

while inputIndex < inputString.length:
    currentChar = inputString[inputIndex]
    if currentState == S0:
        if currentChar.isalpha():
            currentState = S1
            token = currentChar
        elif currentChar in keywordStarters:
            currentState = getKeywordStartState(currentChar)
            token = currentChar
        elif currentChar.isdigit():
            currentState = S5
            token = currentChar
        elif currentChar == '\'':
            currentState = S6
            token = currentChar
        else:
            # 处理其他字符,如运算符、标点符号
            identifyOperatorOrPunctuation(currentChar)
    elif currentState == S1:
        if currentChar.isalnum():
            token += currentChar
        else:
            if token in keywordTable:
                print(token + " 是一个关键字")
            else:
                print(token + " 是一个标识符")
            currentState = S0
            # 像之前一样处理当前非字母数字字符
            if currentChar.isdigit():
                currentState = S5
                token = currentChar
            elif currentChar == '\'':
                currentState = S6
                token = currentChar
            else:
                identifyOperatorOrPunctuation(currentChar)
    # 对于其他状态,如S5(整数构建)、S6(字符串构建)等,有类似逻辑
    inputIndex += 1

总的来说,PL/0编译器中词法分析程序的设计与实现是一个精心构建的过程,充分考虑了该语言的特定语法规则。通过借助有限状态自动机的状态转移表,并遵循系统的标记识别方法,它有效地将PL/0源代码的字符流转换为有意义的标记序列,为编译过程的后续阶段奠定了基础。

三、单词的形式化描述工具

在编程语言处理这个复杂的领域中,精确界定单词的构成规则至关重要。为了实现这一点,形式化描述工具便派上了用场。其中,正则表达式是一种强大且应用广泛的工具。

1. 正则表达式剖析

正则表达式运用一组特定的运算符和操作数来描述单词模式。这些运算符赋予了正则表达式精确描述各种单词结构的灵活性。

  • 基础运算符
    • 连接:这是最基本的运算。当两个表达式相邻放置时,就意味着连接。例如,正则表达式"ab",它表示一个字符'a'紧接着字符'b'的序列。在单词描述的情境中,如果我们有一个由两部分组成的标识符模式,比如"prefix_suffix",我们可以将其看作是"prefix"部分和"suffix"部分的连接,用更详细的正则表达式表示就是"prefix" + "_" + "suffix" 。
    • 选择(|):竖线运算符表示选择。例如,正则表达式"a|b"描述了一种模式,即要么出现字符'a',要么出现字符'b'。在描述编程语言中的关键字时,如果有两个相似的关键字,比如"print"和"println",我们就可以使用类似"print(|ln)"的正则表达式来匹配其中任意一个。
    • 克林闭包(*) :克林闭包运算符表示前面的元素可以出现零次或多次。如前文提到的,用于描述标识符的正则表达式"[a-zA-Z][a-zA-Z0 - 9]",就很好地展示了这个运算符。第一部分"[a-zA-Z]"确保标识符以字母开头,第二部分"[a-zA-Z0 - 9]"允许后面跟随任意数量(包括零个)的字母或数字。所以,像"a""abc123""x9y"这样的标识符都符合这个模式。
    • 正闭包(+):与克林闭包类似,但正闭包运算符要求前面的元素至少出现一次。例如,正则表达式"[0 - 9]+"描述了一个或多个数字的序列。这可以用来表示程序中的正整数,比如"1""123""9999" 。
  • 括号分组:括号用于将正则表达式的部分内容分组。当我们想对特定的子表达式应用运算符时,这非常有用。例如,在正则表达式"(ab)+"中,括号将"ab"分组,正闭包运算符表示"ab"序列必须出现一次或多次。所以,像"ab""abab""ababab"这样的字符串都能匹配这个模式。

2. 在描述不同单词类型中的应用

  • 标识符 :如前所述,正则表达式"[a-zA-Z][a-zA-Z0 - 9]"全面且准确地描述了标识符。在不同的编程语言中,虽然标识符规则可能存在一些细微差异,但这个基本模式始终是核心部分。例如,在Python中,除了基本的字母数字字符外,标识符还允许使用下划线。所以,一个更符合Python标识符规则的正则表达式可以是"[a-zA-Z_][a-zA-Z0 - 9_]" 。
  • 关键字:每种编程语言都有自己的关键字集合。对于像PL/0这样的简单语言,我们可以使用正则表达式对其进行分组和描述。例如,如果考虑关键字"begin""end""if""then",我们可以创建一个正则表达式"begin|end|if|then",来匹配源代码中的这些关键字。
  • 常量
    • 整数常量:对于整数常量,可以使用正则表达式"[-+]?[0 - 9]+" 。"[-+]?"部分是可选的,表示可能存在正号或负号,"[0 - 9]+"确保在符号(如果有)后面至少有一个数字。所以,像"10""-25""333"这样的数字都能匹配这个模式。
    • 字符串常量 :字符串常量通常用特定的定界符括起来。在许多语言中,使用双引号或单引号。例如,要描述一种使用双引号的语言中的字符串常量,可以使用正则表达式""([^\\n]|(\.))?"" 。这里,外层的双引号定义了字符串的开始和结束。内层部分"([^\\n]|(\.))?"匹配任何不是反斜杠或换行符的字符,或者任何前面带有反斜杠的字符(用于处理转义序列)。所以,像"Hello""This is a string with \n newline""a"b"(有适当转义)这样的字符串都能匹配这个模式。

3. 与其他形式化描述工具的比较

虽然正则表达式在描述单词级别的模式方面非常有效,但它并不是唯一可用的形式化描述工具。上下文无关文法(CFGs)是编译器设计工具包中的另一个强大工具。CFGs更适合描述编程语言的整体语法结构,这不仅包括单词级别的构造,还包括单词如何组合形成语句、表达式和程序。例如,一个简单算术表达式的CFGs可能有这样的规则:"表达式 -> 项 + 表达式 | 项",其中"项"可以进一步根据数字和标识符来定义。相比之下,正则表达式专注于单个单词的构建块。然而,在词法分析的背景下,正则表达式对于定义单个词法单元的结构更具针对性和高效性。在许多情况下,它们也更容易实现,因为可以直接将其转换为有限状态自动机,而有限状态自动机是高效词法分析器的基础。

四、有穷自动机

有穷自动机在词法分析中扮演着核心角色,它能够高效地识别正规式所描述的语言。

(一)确定的有穷自动机DFA

DFA是一种确定性的计算模型,它由状态集合、输入符号集合、转移函数、初始状态和终态集合组成。在DFA中,对于每一个状态和输入符号,都有唯一确定的下一个状态。例如,我们构建一个识别数字的DFA,初始状态为S0,当输入数字字符时,从S0转移到S1,在S1状态下继续输入数字字符会保持在S1状态,直到输入非数字字符,若此时处于终态S1,则表示识别出了一个数字。这种确定性使得DFA在词法分析中能够快速准确地识别单词。

(二)不确定的有穷自动机NFA

与DFA不同,NFA在某些情况下对于同一个状态和输入符号可能有多个转移选择,甚至可以有ε转移(即不消耗输入符号的转移)。NFA的灵活性更高,在描述某些复杂的语言模式时更为方便。例如,在识别包含多种可选模式的单词时,NFA可以通过多个并行的路径来表示不同的选择,而不需要像DFA那样将所有情况都合并到单一的转移路径中。

(三)NFA转换为等价的DFA

由于DFA在实现上更加简单高效,我们常常需要将NFA转换为等价的DFA。转换过程主要基于子集构造法。通过将NFA的状态集合的子集作为DFA的状态,构建DFA的转移函数。例如,NFA中有状态S1、S2,当输入某个符号时,从S1可以转移到S3,从S2可以转移到S4,那么在DFA中,由S1和S2组成的子集状态在输入该符号时,会转移到由S3和S4组成的子集状态。通过这样的方式,逐步构建出与NFA等价的DFA。

(四)确定有穷自动机的化简

化简DFA的目的是减少其状态数量,提高运行效率。常用的方法是基于状态的可区分性。如果两个状态对于所有可能的输入序列都能导致相同的结果(到达终态或非终态),那么这两个状态是不可区分的,可以合并为一个状态。例如,DFA中有状态A和B,无论输入何种符号序列,从A和B出发最终到达的状态要么都是终态,要么都是非终态,那么A和B可以合并,从而减少DFA的状态数量。

五、正规式与有穷自动机的等价性

正规式和有穷自动机之间存在着等价关系。一方面,任何一个正规式都可以转换为一个等价的有穷自动机。通过对正规式的结构进行分析,逐步构建有穷自动机的状态和转移关系。例如,对于正规式"a|b",可以构建一个有穷自动机,初始状态有两条转移路径,一条在输入a时转移到终态,另一条在输入b时转移到终态。另一方面,任何一个有穷自动机也都可以用一个正规式来描述其识别的语言。通过对有穷自动机的状态和转移关系进行分析,推导出对应的正规式。这种等价性为词法分析提供了多种实现思路,可以根据具体情况选择使用正规式还是有穷自动机来描述和识别单词。

六、正规文法与有穷自动机的等价性

正规文法同样与有穷自动机等价。正规文法分为右线性文法和左线性文法。以右线性文法为例,它的产生式形式为A→aB或A→a(A、B为非终结符,a为终结符)。我们可以将右线性文法的产生式转换为有穷自动机的状态转移。例如,产生式A→aB可以对应有穷自动机中从状态A输入a转移到状态B的操作,A→a对应从状态A输入a转移到终态的操作。反之,有穷自动机也可以转换为正规文法,通过分析有穷自动机的状态和转移关系,构建正规文法的产生式。这种等价性使得我们在词法分析中可以从不同的角度来描述和处理单词识别问题。

七、词法分析程序的自动构造工具

为了提高词法分析程序的开发效率,有许多自动构造工具可供使用,其中lex是较为常用的一种。

(一)lex描述文件中使用的正规表达式

lex描述文件通过正规表达式来定义单词模式。这些正规表达式与我们前面提到的正规式类似,但在lex中有其特定的语法和扩展。例如,可以使用"+"表示前面的字符或字符组出现一次或多次,用"*"表示出现零次或多次等。通过这些丰富的运算符,可以准确地描述各种复杂的单词模式。

(二)lex描述文件的格式

lex描述文件通常分为三个部分:声明部分、规则部分和辅助函数部分。在声明部分,可以定义一些常量、变量以及包含的头文件等。规则部分则是通过正规表达式和对应的动作来定义单词的识别规则,当识别到符合某个正规表达式的单词时,就执行相应的动作,比如返回单词类型等。辅助函数部分可以编写一些自定义的函数,用于规则部分动作的实现。

(三)lex的使用

使用lex时,首先要编写好lex描述文件,然后通过lex工具对该文件进行处理,生成词法分析器的C代码。例如,在命令行中输入"lex lex_file.l",其中"lex_file.l"是编写好的lex描述文件。lex工具会根据文件中的规则生成相应的C代码,经过编译链接后,就可以得到可执行的词法分析程序。

(四)与yacc的接口约定

yacc是常用的语法分析器生成工具,lex常常与yacc配合使用。在与yacc的接口约定中,lex生成的词法分析器要能够将识别出的单词及其类型传递给yacc进行语法分析。一般通过定义特定的函数和全局变量来实现这种数据传递。例如,lex生成的词法分析器在识别出一个单词后,通过调用特定函数将单词类型和值传递给yacc,yacc根据这些信息进行语法规则的匹配和分析。

通过以上对词法分析的各个方面的介绍,相信大家对词法分析有了更全面深入的理解。从词法分析程序的设计,到各种形式化描述工具、有穷自动机,再到自动构造工具,它们共同构成了词法分析这个重要的编译阶段的完整体系。在实际的编译器开发等工作中,灵活运用这些知识和工具,能够高效地实现词法分析功能,为整个编译流程的顺利进行提供坚实保障。

相关推荐
程序员西西2 天前
SpringBoot无感刷新Token实战指南
java·开发语言·前端·后端·计算机·程序员
搞科研的小刘选手2 天前
【厦门大学主办】第六届计算机科学与管理科技国际学术会议(ICCSMT 2025)
人工智能·科技·计算机网络·计算机·云计算·学术会议
程序员西西2 天前
SpringBoot轻松整合Sentinel限流
java·spring boot·后端·计算机·程序员
程序员西西2 天前
SpringBoot整合JWT实现安全认证
java·计算机·程序员·编程
程序员西西4 天前
SpringCloudGateway入门实战
java·spring boot·计算机·程序员·编程
EndingCoder5 天前
会话管理与Cookie安全
redis·安全·缓存·it·cookie
jay神9 天前
【原创】基于YOLO模型的手势识别系统
深度学习·yolo·计算机·毕业设计·软件设计与开发
程序员鱼皮10 天前
颜色网站为啥都收费?自己做个要花多少钱?
计算机·程序员·互联网·编程·网站
Halo_tjn12 天前
Java 基于分支和循环结构的专项实验
java·开发语言·计算机
云边云科技53412 天前
云边云科技SD-WAN解决方案 — 构建安全、高效、智能的云网基石
网络·科技·安全·架构·it·sdwan