【编译原理】词法分析(编译器、转移图、正则表达式)
【编译原理】词法分析(编译器、转移图、正则表达式)
词法分析是编译器前端的重要组成部分,它将源代码文本分解成一系列标记(tokens),这些标记代表了源代码中的各种语法元素,如关键字、标识符、常量、运算符等。本文将详细介绍词法分析的原理和实现方法,包括转移图的构造、正则表达式的应用以及有限状态自动机(FA)的原理。
编译器的阶段
编译器通常由多个阶段组成,其中词法分析是前端处理的第一个步骤。整个编译过程可以概括为:
源程序→前端→中间表示→后端→目标程序
前端
词法分析器的作用是将源代码的字符流转换为单词流。例如,对于以下代码片段:
if (x > 5)
y = "hello";
else
z = 1;
词法分析器会将其分解为以下词法单元:
- IF - 关键词,表示条件语句的开始。
- GT - 运算符,表示“大于”。
- INT(5) - 整数常量,值为5。
- RPAREN - 右括号,表示条件表达式的结束。
- LPAREN - 左括号,表示条件表达式的开始。
- IDENT(x) - 标识符,代表变量x。
- SEMICOLON - 分号,表示语句的结束。
- IDENT(y) - 标识符,代表变量y。
- ASSIGN - 赋值运算符,用于将值赋给变量。
- STRING("hello") - 字符串常量,值为"hello"。
- ELSE - 关键词,表示条件语句的“否则”部分。
- INT(1) - 整数常量,值为1。
- IDENT(z) - 标识符,代表变量z。
- ASSIGN - 赋值运算符,用于将值赋给变量。
- SEMICOLON - 分号,表示语句的结束。
- EOF - 文件结束标记,表示源代码的结束。
这些词法单元随后会被语法分析器使用,以构建抽象语法树(AST),这是源代码的树状结构表示,用于后续的编译或解释执行。
手工构造法
词法分析器通常采用手工编码的方式实现,通过定义状态机来识别和解析源代码中的各种语法元素。
转移图
转移图是一种有限状态机(FSM),用于识别和解析特定的语法元素。例如,以下转移图描述了一个用于识别关系运算符的FSM:
- 初始状态(start):从初始状态开始,FSM读取第一个字符。
- 状态转移和动作:
- 当读取到
<
时,FSM进入一个新的状态,并再次读取下一个字符。 - 如果下一个字符是
=
,则返回LE
(小于等于)。 - 如果下一个字符是
>
,则返回NE
(不等于)。 - 如果读取到其他字符,则回退到上一个字符,并返回
LT
(小于)。 - 当读取到
=
时,FSM再次读取下一个字符。 - 如果下一个字符是
=
,则返回EQ
(等于)。 - 如果读取到其他字符,则回退到上一个字符,并返回
EQ
(等于)。 - 当读取到
>
时,FSM进入一个新的状态,并再次读取下一个字符。 - 如果下一个字符是
=
,则返回GE
(大于等于)。 - 如果读取到其他字符,则回退到上一个字符,并返回
GT
(大于)。
- 状态转移图:转移图通过状态和转移边来描述FSM的行为。每个状态都有一个或多个转移边,这些转移边由输入字符触发。每个状态也可能有一个或多个动作,这些动作在转移时执行。
- 回退机制:当FSM在某个状态读取到一个不符合预期的字符时,它会回退到上一个字符,并返回相应的关系运算符。
- 动作:每个动作都返回一个关系运算符,如
LE
、NE
、LT
、EQ
、GE
、GT
。
伪代码解释:
token nextToken() {
char c = getChar(); // 读取第一个字符
switch(c) {
case '<':
c = getChar(); // 读取下一个字符
switch(c) {
case '=':
return LE; // 返回小于等于
case '>':
return NE; // 返回不等于
default:
rollback(); // 回退到上一个字符
return LT; // 返回小于
}
case '=':return EQ; // 返回等于
case '>':
c = getChar(); // 读取下一个字符
if (c == '=') {
return GE; // 返回大于等于
} else {
rollback(); // 回退到上一个字符
return GT; // 返回大于
}
default:
return other; // 处理其他字符
}
}
标识符的转移图
这个FSM用于解析程序代码中的标识符,例如变量名、函数名等。标识符通常由字母、数字、下划线组成,且不能以数字开头。
- 初始状态:从初始状态开始,FSM读取第一个字符。
- 状态转移和动作:
- 当读取到一个字母(a-z或A-Z)或下划线(_)时,FSM进入一个新的状态,并继续读取下一个字符。
- 如果读取到其他字符,则返回标识符(ID)并结束解析。
- 循环读取:在新状态中,FSM继续读取字符,直到读取到的字符不再是字母、数字或下划线。这个过程是一个循环,直到遇到一个不符合条件的字符。
- 动作:当FSM读取到一个符合条件的字符序列后,它会返回一个标识符(ID)。
伪代码解释:
token nextToken() {
char c = getChar(); // 读取第一个字符
switch(c) {
case 'a', 'b', ..., 'z', 'A', ..., 'Z', '_':
c = getChar(); // 读取下一个字符
while(c == 'a' || c == 'b' || ... || c == 'z' || c == 'A' || ... || c == 'Z' || c == '0' || ... || c == '9' || c == '_') {
c = getChar(); // 继续读取下一个字符
}
return ID; // 返回标识符
default:
return other; // 处理其他字符
}
}
关键字表算法
这个FSM用于解析程序代码中的关键字,关键字是编程语言中预定义的、具有特殊意义的标识符,如 “if”、“else”、“while” 等。
- 初始状态:从初始状态开始,FSM读取第一个字符。
- 状态转移和动作:
- 当读取到一个字母(a-z或A-Z)或下划线(_)时,FSM进入状态1。
- 在状态1,如果读取到的字符是i,则转移到状态2。
- 在状态2,如果读取到的字符是f,则转移到状态3。
- 在状态3,如果读取到的字符是字母、数字或下划线,则继续读取下一个字符,直到读取到一个非这些字符的字符。
- 如果在任何状态中读取到的字符不是预期的字符,FSM将返回到状态other并返回ID(标识符)。
- 结束状态:在状态3,如果读取到的字符不是字母、数字或下划线,FSM将确认已经读取到完整的关键字 “if” 并返回关键字。
- 动作:当FSM读取到一个符合条件的字符序列后,它会返回相应的关键字或标识符。
伪代码解释:
token nextToken() {
char c = getChar(); // 读取第一个字符
if (isLetter(c) || c == '_') {
c = getChar(); // 读取下一个字符
if (c == 'i') {
c = getChar(); // 读取下一个字符
if (c == 'f') {
c = getChar(); // 继续读取下一个字符
while (isLetter(c) || isDigit(c) || c == '_') {
c = getChar(); // 继续读取下一个字符
}
if (!isLetter(c) && !isDigit(c) && c != '_') {
return "if"; // 返回关键字 "if"
}
}
}
}
return ID; // 返回标识符
}
正则表达式
正则表达式是一种强大的模式匹配工具,可以用来描述词法分析器的规则。以下是一些正则表达式的概念和示例:
自动生成
正则表达式可以用来自动生成词法分析器的规则。
什么是正则表达式
正则表达式是一种描述字符串模式的符号语言,可以用来匹配、查找和处理文本。它由普通字符(如字母和数字)以及特殊字符(称为元字符)组成,这些元字符具有特殊的含义。
Kleene闭包
在正则表达式中,闭包(也称为星号运算符或 Kleene 星号)表示一个模式可以出现零次或多次。这个运算符用星号(*)表示。当应用到某个正则表达式上时,它意味着该表达式可以匹配零个或多个该模式。
举例解释:
- 简单字符闭包:
- 正则表达式
a*
表示字符 ‘a’ 可以出现零次或多次。 - 例如,对于字符串 “aaabaaa”,
a*
可以匹配整个字符串,也可以只匹配 “a”, “aa”, “aaa” 等。
- 字符组合闭包:
- 正则表达式
ab*
表示字符 ‘a’ 后面跟着零个或多个 ‘b’。 - 例如,它可以匹配 “a”, “ab”, “abb”, “abbb” 等。
- 复杂模式闭包:
- 正则表达式
(ab|cd)*
表示 ‘ab’ 或 ‘cd’ 可以出现零次或多次。 - 例如,它可以匹配空字符串,“ab”, “cd”, “abab”, “cdcd”, “ababab”, “cdcdcd” 等。
- 数字模式闭包:
- 正则表达式
\d*
表示数字(0-9)可以出现零次或多次。 - 例如,它可以匹配任何不包含数字的字符串,也可以匹配 “123”, “456”, “7890” 等。
- 实际应用示例:
- 假设我们需要匹配一个可能包含多个空格的字符串。正则表达式
\s*
可以匹配零个或多个空白字符(包括空格、制表符等)。 - 例如,它可以匹配 “hello”, “hello world”, " hello world " 等。
给定字符集Σ = { a , b },我们可以构造多种正则表达式。正则表达式可以是单个字符、空串、或者通过选择(或)、连接和闭包操作符组合字符形成的更复杂的模式。以下是一些基于给定字符集的正则表达式示例:
- 单个字符:
a
b
- 空串:
- ε(表示空字符串)
- 选择(或):
a|b
(匹配 ‘a’ 或 ‘b’)
- 连接:
ab
(匹配 ‘a’ 后跟 ‘b’)ba
(匹配 ‘b’ 后跟 ‘a’)
- 闭包:
a*
(匹配 ‘a’ 出现零次或多次)b*
(匹配 ‘b’ 出现零次或多次)
- 组合使用:
a|a*
(匹配 ‘a’ 或者 ‘a’ 出现零次或多次)b|b*
(匹配 ‘b’ 或者 ‘b’ 出现零次或多次)(a|b)*
(匹配 ‘a’ 或 ‘b’ 出现零次或多次的任意序列)
- 更复杂的组合:
a(b|a)*
(匹配 ‘a’ 后跟 ‘b’ 或 ‘a’ 的任意序列)b(a|b)*
(匹配 ‘b’ 后跟 ‘a’ 或 ‘b’ 的任意序列)(a|b)+
(匹配 ‘a’ 或 ‘b’ 出现一次或多次的序列)
- 嵌套闭包:
(a*|b*)*
(匹配 ‘a’ 或 ‘b’ 出现零次或多次的序列,且该序列可以重复零次或多次)
语法糖
语法糖是指那些在编程语言中为了提高代码可读性和简洁性而设计的语法特性,它们通常可以被更基础的语法结构所替代。
有限状态自动机(FA)
有限状态自动机(Finite Automaton,简称FA)是一种抽象的计算模型,用于识别和处理字符串。它由一组状态、一个初始状态、一个或多个接受状态以及一组转移函数组成。
DFA(确定性有限自动机):
转移函数:
(q_0, a) → q_1
:从状态q_0
读取 ‘a’ 转移到状态q_1
。(q_0, b) → q_0
:从状态q_0
读取 ‘b’ 仍然留在状态q_0
。(q_1, a) → q_2
:从状态q_1
读取 ‘a’ 转移到状态q_2
,这是一个接受状态。(q_1, b) → q_1
:从状态q_1
读取 ‘b’ 仍然留在状态q_1
。(q_2, a) → q_2
:从状态q_2
读取 ‘a’ 仍然留在状态q_2
。(q_2, b) → q_2
:从状态q_2
读取 ‘b’ 仍然留在状态q_2
。
要使字符串被接受,FSM 必须最终停在接受状态 q_2
。根据转移函数,只有从状态 q_1
读取 ‘a’ 才能转移到接受状态 q_2
。因此,任何被接受的字符串必须至少包含一个 ‘a’ 来从 q_0
转移到 q_1
,并且至少包含另一个 ‘a’ 来从 q_1
转移到 q_2
。
被接受的字符串模式包括:
- 至少包含两个 ‘a’。
- 第一个 ‘a’ 使得 FSM 从
q_0
转移到q_1
。 - 第二个 ‘a’ 使得 FSM 从
q_1
转移到q_2
,这是一个接受状态。 - 除了这两个 ‘a’ 之外,字符串中可以包含任意数量的 ‘b’,并且这些 ‘b’ 可以在第一个 ‘a’ 之前、两个 ‘a’ 之间,或者在第二个 ‘a’ 之后。
因此,被接受的字符串包括:
- “aa”(最少情况下,直接满足条件)
- “ababa”
- “baa”
- “baab”
- “bbaab”
- “aab”
- “bbbaabb”
- 等等。
只要字符串中至少有两个 ‘a’,并且这两个 ‘a’ 之间的字符可以是任意数量的 ‘b’,那么这个字符串就会被接受。
什么叫接受?
最后到达双圈的接受状态。
非确定的:(NFA)
将NFA转化为与之等价的DFA
小结
幂集就是一个集合所有子集构成的集合。
本文原文来自CSDN