Skip to content

Commit f9418fa

Browse files
committed
add lexer note
1 parent 94028ec commit f9418fa

11 files changed

+209
-25
lines changed

png/compiler_principle/image-12.png

518 KB
Loading

png/compiler_principle/image-13.png

532 KB
Loading

png/compiler_principle/image-14.png

778 KB
Loading

png/compiler_principle/image-15.png

373 KB
Loading

png/compiler_principle/image-16.png

871 KB
Loading

png/compiler_principle/image-17.png

687 KB
Loading

png/compiler_principle/image-18.png

509 KB
Loading

source/llvm/compiler_principle.md

+156-21
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,80 @@
11
# 编译原理笔记
22
- [编译原理_中科大(华保健)](https://www.bilibili.com/video/BV16h411X7JY?p=1&vd_source=fcb5a5bbba15e874747108473094add4)
33

4-
## 词法分析
4+
## Lexical Analysis 词法分析
55

6+
```
67
字符流-->词法分析器-->记号流
7-
手工构造:转移图算法(关键字哈希表算法,o(1))
8-
自动生成:正则表达式
8+
```
9+
- 手工构造:转移图算法(关键字和标识符重叠的情况:关键字哈希表算法,o(1),详见参考文章1)
10+
- 自动生成:声明式规范(正则则表达式) => 某工具 => 词法分析器
911

1012
参考文章:
11-
https://juejin.cn/post/7203285698073116727
12-
https://www.tr0y.wang/2021/04/01/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90/
13+
- https://juejin.cn/post/7203285698073116727
14+
- https://www.tr0y.wang/2021/04/01/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9A%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90/
1315

1416
### 正则表达式
1517

16-
17-
![alt text](../../png/compiler_principle/image.png)
18+
```
19+
对于给定的字符集:Σ = {c1, c2, ..., cn},有
20+
1. ε(空字符串)是正则表达式
21+
2. 对于任意 c ∈ Σ,c 是正则表达式
22+
3. 如果 M、N 是正则表达式,那么以下都是正则表达式
23+
- 选择:M | N == {M, N}
24+
- 连接:MN == {mn | m ∈ M, n ∈ N}
25+
- 闭包:M* == {ε, M, MM, MMM, ...}
26+
```
1827

1928
基本规则,归纳规则
2029

21-
正则表达式-->flex-->有限状态自动机
30+
正则表达式-->某工具:flex-->有限状态自动机
2231

2332
![alt text](../../png/compiler_principle/image-1.png)
2433
![alt text](../../png/compiler_principle/image-2.png)
2534

2635
给定一个字符串,从起始状态,经过转移函数,最终都可以走到终结状态-->“接受”
2736

28-
- 确定有限状态自动机(DFA):最多只有一个状态可以转移
29-
- 非确定有限状态自动机(NFA)
37+
- 确定有限状态自动机(DFA):对于每个字符,最多只有一个状态可以转移
38+
- 非确定有限状态自动机(NFA):对于每个字符,有多于一个状态可以转移。DFA 与 NFA 主要的区别就在于判断 “接受” 的难度不同。NFA 不能直接回答 “no”,而是需要回滚路线,重新尝试,直到所有可能路线都无法接受,才能回答 “no”。
3039

3140

3241
RE-->NFA-->DFA-->词法分析器的代码表示
3342

34-
### Tompson算法:RE-->NFA
43+
#### 1.RE-->NFA:Tompson算法
44+
45+
- 对于基本的 re(上一节1,2情况),直接构造
46+
- 对于复合的 re(上一节3情况),递归构造
47+
3548
![alt text](../../png/compiler_principle/image-3.png)
3649
![alt text](../../png/compiler_principle/image-4.png)
3750

3851

39-
### 子集构造算法:NFA-->DFA
52+
#### 2.NFA-->DFA:子集构造算法
4053

41-
![alt text](../../png/compiler_principle/image-5.png)
54+
伪代码和示例如下:
4255

43-
- 不动点算法,O(2^N)
56+
![alt text](../../png/compiler_principle/image-5.png)
57+
58+
- 不动点算法原理,O(2^N)
4459
- 步骤:
45-
- delta(q):基于q(n-1)节点中所有n节点,求q(n)
46-
- 再求e-闭包:扩展求所有e连接的点。深度/广度算法都行,如下:
60+
- delta(q):基于q(n-1)节点中所有状态集,遍历每一个字符(不能是ε),求q(n)
61+
- 再求e-闭包:扩展求所有ε连接的点。深度/广度算法都行,如下:
4762
![alt text](../../png/compiler_principle/image-6.png)
4863

49-
DFA最小化算法:Hopcroft算法、
64+
算法一定会运行结束吗?因为其实最多只会生成 2^n 个状态集,n 是状态转移图里的状态数量,所以这个算法q生成是单调递增的,如果收集所有状态集就会停止了,并且这是最糟糕的情况,实际情况中,不太可能有所有状态的组合。例如我们上面那个例子,状态转移图里的状态数量一共是 10 个,理论上会生成 2^10 = 1024 个不同的 q,肯定没有这么多。
65+
66+
#### 3.DFA最小化算法:Hopcroft算法
5067

5168
![alt text](../../png/compiler_principle/image-7.png)
5269

5370
- 先根据非终结状态,终结状态节点划分成两个等价类
54-
- 对每个等价类中的每个节点,查看是否有转移外面的,可以继续切分
71+
- 对遍历等价类中的每个节点,查看该节点是否有转移到外面的相同等级类可以合并;不同等价类,可以继续切分
5572

5673
![alt text](../../png/compiler_principle/image-8.png)
5774

58-
### 词法分析器的代码表示:DFA的生成算法
75+
#### 4.词法分析器的代码表示:DFA的生成算法
5976

60-
- 转移表
77+
- 转移表
6178

6279
![alt text](../../png/compiler_principle/image-9.png)
6380

@@ -68,4 +85,122 @@ DFA最小化算法:Hopcroft算法、
6885

6986
- 跳转表
7087

71-
![alt text](../../png/compiler_principle/image-11.png)
88+
![alt text](../../png/compiler_principle/image-11.png)
89+
90+
91+
## Lexical Analysis 语法分析
92+
93+
引用:
94+
95+
- https://www.tr0y.wang/2021/04/02/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E4%B9%8B%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E5%88%86%E6%9E%90%E7%AE%97%E6%B3%95/
96+
97+
```
98+
记号流(token sequence) -->语法分析器(parser) -->语法树( abstract syntax tree)
99+
100+
语言的规则(language syntex)
101+
```
102+
乔姆斯基文法体系给出了 4 类文法:
103+
104+
- 0型文法:任意文法,在程序设计语言当中不常用
105+
- 1型文法:上下文有关文法,在程序设计语言当中也不常用
106+
- 2型文法:即 CFG,这种文法可以用来描述语言的语法结构。
107+
- 3型文法:又叫做正则文法,其实词法分析器已经讨论过了,即正则表达式,这种文法可以用来描述语言的词法结构。
108+
109+
在这些文法中,0型文法的表达能力最强,3型文法最弱
110+
111+
### 上下文无关文法(CFG)
112+
113+
![alt text](../../png/compiler_principle/image-18.png)
114+
115+
### CFG 的推导
116+
117+
推导的定义:给定文法 G,从 G 的开始符号 S 开始,用产生式的右部替换左侧的非终结符,不断重复,直到不出现非终结符为止。最后的得到的结果,我们把它称为**句子**
118+
119+
推导的方式,根据替换的顺序,分为 2 种:
120+
- 最左推导: 每次总是选择最左侧的符号进行替换
121+
- 最右推导: 每次总是选择最右侧的符号进行替换
122+
123+
由此我们可以得出语法分分析的具体含义:给定文法 G 和句子 s,回答是否存在对句子 s 的推导
124+
125+
语法分析器的输入是记号流,其实就是句子 s;而判断句子是否符合语法规则,就可以利用文法 G 来做判断。
126+
127+
### 分析树
128+
推导过程,实际上可以表达成**树状结构**
129+
130+
![alt text](../../png/compiler_principle/image-12.png)
131+
132+
每个内部节点代表的是非终结符;叶子节点代表终结符;每一步推导代表如何从双亲节点生成它的直接孩子节点。如上图的1,2,3,4,5步
133+
134+
### 推导的二义性
135+
136+
![alt text](../../png/compiler_principle/image-13.png)
137+
138+
通过对比可以看出,分析树的含义取决于对树进行后序遍历的结果。
139+
- 对于第一种方式来说,应该是 3 + (4 * 5) = 23;
140+
- 对于第二种方式来说,是 (3 + 4) * 5 = 35。
141+
按照我们的常识来说,肯定是第一种才是我们想要的结果。
142+
143+
所以这个文法就是 二义性文法:给定文法 G,如果存在句子 s,它有两棵不同的分析树(同样是最左推导),那么称 G 是二义性文法。
144+
145+
#### 消除左递归
146+
147+
重写文法就可以解决二义性。对于上面这个例子来说,我们想要先计算 *,则可以将推导过程分解为先计算 T = F * F,然后在计算 E = T + T。
148+
149+
![alt text](../../png/compiler_principle/image-14.png)
150+
151+
#### 自顶向下分析算法实现
152+
上面说的分析方式,都是从开始符号出发推出句子,因此称为自顶向下分析,对应于分析树就是自顶向下的构造顺序。(BFS思路)
153+
154+
自顶向下分析算法的定义:
155+
156+
目标:给定文法 G 和句子 s,回答 s 是否能够从 G 推导出来?
157+
158+
基本算法思想:从 G 的开始符号出发,随意推导出某个句子 t,比较 t 和 s
159+
若 t == s,则回答 “是”;若 t != s,则回溯。
160+
161+
需要注意的是,如果 t != s,我们是不能直接回答 “否” 的,因为 t 有很多种可能性,所以只有当 G 可以推导出的所有句子 t 都不等于 s 的时候,我们才能回答 “否”。
162+
163+
![alt text](../../png/compiler_principle/image-16.png)
164+
165+
- 需要向前尝试,向后回溯。分析效率昂贵
166+
- 需要线性时间的算法:避免回溯。引入:
167+
- 递归下降分析算法:手动
168+
- LL(K)算法:自动
169+
- LR(K)算法:自动
170+
171+
172+
### 递归下降分析算法(prediction/ top-down parsing)
173+
174+
递归下降分析算法 也称为预测分析。
175+
176+
- 它分析高效(线性时间),
177+
- 容易实现(方便手工编码),
178+
- 错误定位和诊断信息准确,被很多开源和商业的编译器所采用,比如 GCC4.0, LLVM, ...。
179+
180+
算法基本思想:(**分治法**
181+
- 每个非终结符都要实现一个相应的parse分析函数
182+
- 在parse函数中用**前看符号(token)**指导产生式规则的选择,避免回溯。- parse函数的实现:遇到产生式规则中的非终结符递归调用,遇到比较终结符比较,比较不到报错
183+
- 条件:每个右部都应该不相交的
184+
185+
![alt text](../../png/compiler_principle/image-15.png)
186+
187+
#### 消除完左递归的递归下降分析
188+
两者产生式规则写法:
189+
```
190+
E -> E + T
191+
| T
192+
T -> T * F
193+
| F
194+
F -> num
195+
| id
196+
```
197+
<===>
198+
```
199+
E -> T (+ T)*
200+
T -> F (* F)*
201+
F -> num
202+
```
203+
204+
![alt text](../../png/compiler_principle/image-17.png)
205+
206+
### LL(K)

source/llvm/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ llvm-learning-notes's documentation!
1414
llvm_ir
1515
vtable
1616
llvm_value
17+
compiler_principle
1718

1819
Indices and tables
1920
==================

source/llvm/vbase.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#! https://zhuanlan.zhihu.com/p/699236872
12
# c++虚继承在llvm ir的形式
23

34
## 多重继承
@@ -8,8 +9,6 @@
89

910
在普通的继承或多重继承中,一个子类和他的基类、基类的基类、...的成员变量都是嵌入在一起的,虚函数放在一个vtable上,多重继承有多个vptr。如果存在菱形继承的场景,就会导致基类的数据和虚函数存在多个。如下例子所示。
1011

11-
https://godbolt.org/z/KzPd4x6s9
12-
1312
```c++
1413
class A {
1514
public:
@@ -96,9 +95,11 @@ d --> +----------+ | | B::w() |
9695
9796
可以看到构造的时候是对每个基类平铺而不是嵌套了。
9897
98+
我们再看下在llvm ir中的形式。
99+
99100
注意这里struct D的数据内存布局, 按照B、C、D、A的顺序。虚基类放在最后。与普通的继承,自身D放在最后不同。
100101
101-
按照8字节对齐
102+
并且按照8字节对齐。
102103
103104
```mlir
104105
%struct.D = type { %struct.B.base, [4 x i8], %struct.C.base, i64, %struct.A.base }

source/other/c++.md

+48-1
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,51 @@ int main()
6161

6262
### PODType
6363

64-
- std::is_trivial && std::is_standard_layout == true
64+
- std::is_trivial && std::is_standard_layout == true
65+
66+
67+
## C++ 中,“常量指针”(指向常量的指针)和“指针常量”(常量指针)
68+
### 常量指针(指向常量的指针)
69+
指向常量的指针意味着指针所指向的值是常量,不能通过该指针修改值。`const int* ptr;`
70+
71+
```cpp
72+
#include <iostream>
73+
74+
int main() {
75+
int value = 10;
76+
const int* ptr = &value; // 指向常量的指针
77+
78+
std::cout << "Value: " << *ptr << std::endl;
79+
80+
// *ptr = 20; // 错误:不能通过指向常量的指针修改值
81+
82+
value = 20; // 直接修改值是允许的
83+
std::cout << "Value: " << *ptr << std::endl;
84+
85+
return 0;
86+
}
87+
```
88+
在这个示例中,ptr 是一个指向常量的指针,你不能通过 *ptr 来修改 value 的值。
89+
90+
### 指针常量(常量指针)
91+
常量指针意味着指针本身是常量,一旦指向了某个地址,不能再指向其他地址。语法:`int* const ptr;`
92+
93+
```cpp
94+
#include <iostream>
95+
96+
int main() {
97+
int value1 = 10;
98+
int value2 = 20;
99+
int* const ptr = &value1; // 指针常量
100+
101+
std::cout << "Value1: " << *ptr << std::endl;
102+
103+
*ptr = 30; // 允许通过指针常量修改值
104+
std::cout << "Modified Value1: " << *ptr << std::endl;
105+
106+
// ptr = &value2; // 错误:不能修改指针常量的地址
107+
108+
return 0;
109+
}
110+
```
111+
在这个示例中,ptr 是一个常量指针,它指向 value1,你不能将 ptr 指向其他地址,但是可以通过 *ptr 修改 value1 的值。

0 commit comments

Comments
 (0)