#词法分析
目录
编译器的阶段
源程序→前端→中间表示→后端→目标程序
前端
其中词法分析器的作用:
字符流到单词流的切分或转换
词法分析是编译器前端的一个重要步骤,它将源代码文本分解成一系列标记(tokens),这些标记代表了源代码中的各种语法元素,如关键字、标识符、常量、运算符等。
c
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
。
- 每个动作都返回一个关系运算符,如
伪代码解释:
c
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
)。
- 当FSM读取到一个符合条件的字符序列后,它会返回一个标识符(
伪代码解释:
c
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通过逐步读取字符并根据读取的字符序列返回相应的标识符,从而实现对标识符的识别和解析。这个过程确保了标识符的正确性,例如不允许以数字开头,且只能包含字母、数字和下划线。
这个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读取到一个符合条件的字符序列后,它会返回相应的关键字或标识符。
伪代码解释:
c
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; // 返回标识符
}
这个FSM通过逐步读取字符并根据读取的字符序列返回相应的关键字或标识符,从而实现对关键字 "if" 的识别和解析。这个过程确保了关键字的正确性,例如不允许关键字后面直接跟字母、数字或下划线,除非它们是关键字的一部分。
关键字表算法
正则表达式
自动生成
如何去写声明式的规范
什么是正则表达式
指出规则 产生出词法分析器
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 } \Sigma = \{a, b\} Σ={a,b},我们可以构造多种正则表达式。正则表达式可以是单个字符、空串、或者通过选择(或)、连接和闭包操作符组合字符形成的更复杂的模式。以下是一些基于给定字符集的正则表达式示例:
-
单个字符:
a
b
-
空串:
- ε \varepsilon ε(表示空字符串)
-
选择(或):
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' 出现零次或多次的序列,且该序列可以重复零次或多次)
0|(1-9)(0-9)*
语法糖
有限状态自动机(FA)
(DFA)
转移函数:
- ( (q_0, a) \rightarrow q_1 ):从状态 ( q_0 ) 读取 'a' 转移到状态 ( q_1 )。
- ( (q_0, b) \rightarrow q_0 ):从状态 ( q_0 ) 读取 'b' 仍然留在状态 ( q_0 )。
- ( (q_1, a) \rightarrow q_2 ):从状态 ( q_1 ) 读取 'a' 转移到状态 ( q_2 ),这是一个接受状态。
- ( (q_1, b) \rightarrow q_1 ):从状态 ( q_1 ) 读取 'b' 仍然留在状态 ( q_1 )。
- ( (q_2, a) \rightarrow q_2 ):从状态 ( q_2 ) 读取 'a' 仍然留在状态 ( q_2 )。
- ( (q_2, b) \rightarrow 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
小结
幂集 就是一个集合所有子集构成的集合