编译原理 龙书 笔记

DFA NFA CFG

DFA的定义:

A = ( Σ, S, s0, F, N )
Σ: 输入字母表(alphabet),是一个输入字符的集合。
S:状态的集合s0:
初始状态F:终止状态集合 F ⊆ S
N:转换公式 N:S×Σ → S

NFA(Non-Deeterministic Finite State Automata)不确定的有穷自动机: 对一个输入符号,有两种或两种以上可能对状态,所以是不确定的
NFA可以转换成DFA,NFA和DFA的主要区别在于:

  • DFA没有输入空串之上的转换动作;

  • 对于DFA,一个特定的符号输入,有且只能得到一个状态,而NFA就有可能得到一个状态集;

正则表达式可以用dfa识别,每一个正则语言都是一个上下无关语言,但反之不成立。

例如 a n b n a^nb^n anbn 不能用正则语言描述,但可以用上下文无关语言描述
在这里插入图片描述

上下文无关语法cfg,就是由终结符,非终结符,产生式和开始符号组成的语法。
{ w c w ∣ w ∈ ( a ∣ b ) ∗ } \{wcw | w \in (a|b)^{*}\} { wcww(ab)}就不属于上下文无关语法,比如标识符的声明和使用,中间被程序段给隔开。

模拟NFA

在这里插入图片描述
在这里插入图片描述

正则表达式转DFA

正则表达式(a|b)*abb的正则表达式,#为结束符
在这里插入图片描述
叶子节点为字符并且每个字符分配一个编号,非叶子节点为连接,星号和或运算符。
在这里插入图片描述
nullable(n)判断以n节点为根的子树能否生成空串
firstpos(n)是子树生成的所有句子的开头字符的集合(按编号计算)
lastpos(n)类似于firstpos(n)

followpos(i) 用于记录编号i的后继编号,
如果n是一个连接节点,左右子节点为c1,c2,那么对于lastpos(c1)中的每个编号i,firstpos(c2)中的所有编号都在followpos(i)中。
如果n是星号节点,并且i是lastpos(n)中的一个,那么firstpos(n)中所有编号都在followpos(i)中。

构造DFA,其中每一个状态都是编号的集合:
在这里插入图片描述

first 集

FIRST 集的提出是为避免用户源码可匹配产生式中“多个”候选式的一种辅助手段

X可以是任意符号串
(1)如果X是终结符,则FIRST(X) = { X } 。
(2)如果X是非终结符,且有产生式形如X → a…,则FIRST( X ) = { a }。
(3) 如果X是非终结符,且有产生式形如X → ABCdEF…(A、B、C均属于非终结符且包含 ε,d为终结符),需要把FIRST( A )、FIRST( B )、FIRST( C )、FIRST( d )加入到 FIRST( X )中。
(4)如果X经过一步或多步推导出空字符ε,将ε加入FIRST( X )。

follow集

FOLLOW(A)是指出现在非终结符 A 之后的终结符或#,#表示输入结束符,类似文件结束符 EOF,表示源码都处理完了,#是为语法分析方便而人为加上的符号,产生式中并不存在 。 FOLLOW 集是为解决非终结符匹配空候选项后能用其后的终结符匹配用户源码而提出的

计算文法符号 X 的 FOLLOW(X) ,不断运用以下规则直到没有新终结符号可以被加入任意FOLLOW集合为止 :
(1)将# 加入到FOLLOW(S)中,其中S是开始符号,而#是输出右端的结束标记。
(2)如果存在一个产生式S->αXβ,那么将集合FIRST(β)中除ε外的所有元素加入到FOLLOW(X)当中。
(3)如果存在一个产生式 S->αX , 或者S->αXβ且FIRST(β)中包含ε , 那么将集合FOLLOW(S)中的所有元素加入到集合FOLLOW(X)中。

LL(1)文法

直接左递归(Immediate left recursion)以下面的句型规则出现:
在这里插入图片描述
间接左递归:
在这里插入图片描述
在这里插入图片描述

(1 )文法中不含左递归
(2 )文法中每个非终结符的各候选式 a 的 FIRST(a)互不相交
(3 )文法中的每个非终结符 A,若它存在一个 ε 候选式,另一个候选式为b那么 FIRST(b)和 FOLLOW(A)交集 为空。

满足以上 3 条的文法即是 LL( l )文法,其中的第一个 L 表示从左到右扫描源码串,第二个 L
表示最左推导( 即每次推导产生式右部中最左边的非终结符), 1 表示分析时的每一步只需要向前
查看 1 个符号。
最左规约就是每次都规约句柄

LL(1)文法属于自上而下的左推导,接下来大致描绘一下使用LL(1)是如何解析:

红色文字:采取的操作 浅蓝色文字:当前待替换的文法串黑色文字:当前待匹配的的token序列
浅绿色文字:判断非终结符能否以目标token开头,或者为空串时,后面能否紧跟着目标token

在这里插入图片描述

  • 转成表格
    在这里插入图片描述

1.拿Parsing queue中的第一个元素和Input中的第一个token做匹配
2.如果Parsingqueue第一个元素是非终结符,则从预测分析表中找到对应的文法推导式,将非终结符替换为推导式右边的部分,回到步骤1。如找不到合适的推导式,则跳到步骤4
3.如果Parsingqueue第一个元素是终结符,则与Input中的第一个token做匹配,并消去Parsingqueue中的第一个元素,回到步骤1。如不匹配则跳到步骤
4.报错
5.当Parsing queue和Input都为空时,则匹配成功,结束

一般来说文法的数量是有限的,目前我们可以手动找,比如我们可以整理出下面的表格,整理出当每种非终结符在Parsing queue顶部时,遇到不同输入所采用的推导式
在这里插入图片描述

  • 构造表格的方法

对于文法G的每个产生式 A->α ,进行如下处理
(1)对于FIRST(α)中每个终结符号a,将 A->α 加入到 M[A,a] 中。
(2)如果 ε在FIRST(α)中,那么对于FOLLOW(A)中每个终结符号b,将 A->α 加入到 M[A,b] 中。如果
ε在FIRST(α),且#在FOLLOW(A)中,也将 A->α 加入到 M[A,#] 中。

推导,归约, 递归,左结合,优先级

句型可能包含非终结符
句子不包含非终结符
短语是非终结符的完整展开项(展开任意次数)
句柄是当前句型中最左边的直接短语(展开一次)
最右推导就是在推导过程中每次都先推导最右边的非终结符
二义性就是对某句子有多个最左推导或最右推导
提取左公因式就是对两个产生式右端开头相同,无法做出选则的情况 这里假设有A->aB1 | aB2,则
在这里插入图片描述

消除左递归:

  • 立即左递归的情况
    在这里插入图片描述
  • 非立即左递归
    如 S -> Aa | b , A -> Sc | d,需要把第二个产生式中的S替换为Aa, 然后转化为立即左递归解决
    写成形式化语句就是:

在这里插入图片描述

  • 左结合和优先级
    四则运算的BNF语法规则
    同样优先级的运算符是从左到右计算还是从右到左计算称为运算符的结合性(Associativity)=是右结合的,如果一个表达式中出现多个=号,不是从左到右计算而是从右到左计算,例如 total=total_minute=hour60+minute,计算的顺序是先算hour60+minute,得到一个结果,然后算右边的等号,就是把计算结果赋给变量total_minute,在算左边的等号,即把这个值赋给变量total。
    (1)不考虑左结合和优先级为

    S -> exp
    exp -> exp opt exp
    exp -> (exp) | alphabet
    opt -> + | - | * | /
    

    (2)考虑左结合

    S -> exp
    exp -> exp opt item | item
    item -> (exp) | alphabet
    opt -> + | - | * | /
    

    (3)考虑左结合和优先级

    S -> AddExp
    AddExp -> AddExp opt1 MulExp | MulExp
    opt1 -> + | -
    MulExp -> MulExp opt2 AtomicExp | AtomicExp
    opt2 -> * | /
    AtomicExp -> (AddExp) | alphabet
    

    有点类似于逆波兰表达式,优先级越高的运算符越早和alphabet结合,越早出现在运算符的左边。从中缀表达式转为后缀(逆波兰)表达式,需要一个栈记录操作符,还有一个输出串。计算后缀表达式,只需要一个栈用来存放操作数和中间值,并且后缀表达式中没有括号。

符号优先文法

任何产生式中都没有两个非终结符相邻的情况,就是符号优先文法
性质:如果Ab或bA出现在某句型r中,A为非终结符,则r中任何含b的短语必定含A。

以以下文法为例:

    E→E+T|T

    T→T*F|F

    F→(E)|i

构建优先关系表:

在文法中添加E→#E#。
其中last(A)是A生成的所有句型的结束终结符组成的集合。
从文法中找出形为aQb(终结符+非终结符+终结符)和ab(终结符+终结符)的部分,本例中为(E)和#E#,然后在(和)与#和#相应的表格填=

从文法中找出形为aQ(终结符+非终结符)的部分,a与Q的FIRSTVT集合中每一个元素在表格中的交叉点填小于号。

在这里插入图片描述

从文法中找出形为Qa(非终结符+终结符)的部分, Q的LASTVT集合中每一个元素与a在表格中的交叉点填大于号。
在这里插入图片描述
动态规划:
首先遍历所有产生式,若有A->a或A->Ba 则二位数组F[A,a]为真。并把所有为真的压栈。
当栈不为空时,弹出栈顶F[A,a],若B->A则使F[B,a]为真,并压栈,重复至栈空。

在这里插入图片描述
这里省去了T->F 和E->T, 因为非终结符对终结符之间的优先级比较没有影响。 只要当前符号小于栈顶,说明栈里一定存在可归约短语。
现在的关键就是如何找到最左素短语(至少包含一个终结符,并且除自身外不包含其它素短语)

在这里插入图片描述

LR文法

  • 和LL文法的区别是得到的是最右推导序列,并且属于自底向上分析
  • 前缀,它是指一个最右句型,没有超过其最右句柄(最右边短语)的某前缀
  • “项”和可行前缀对应,若果存在 S → α A w → α β 1 β 2 w S\rightarrow\alpha Aw\rightarrow\alpha\beta_{1}\beta_{2}w SαAwαβ1β2w,就可以说项 A → β 1 ⋅ β 2 A\rightarrow\beta_{1}\cdot\beta_{2} Aβ1β2对可行前缀 α β 1 \alpha\beta_{1} αβ1有效, ⋅ \cdot 符号表示前缀正好包含 ⋅ \cdot 符号之前的部分短语。
  • 状态是项的集合,action函数决定当且是规约还是移入,goto决定规约后状态的转移
  • LR(0)和LR(1)的区别是: 假设当前状态包含 R → L ⋅ R\rightarrow L\cdot RL,且当前符号栈顶为L。在LR(0)中只要下一个输入符号满足在集合FOLLOW(R)就进行规约,但是LR(1)要求下一个符号为特定值。好处是可以排除不正确的规约。所以项的一般形式编变为 [ A → α ⋅ β , a ] [A\rightarrow\alpha\cdot\beta,a] [Aαβ,a]
    在这里插入图片描述
    在这里插入图片描述
    移入时同事压入终结符和新状态
    规约时弹出相同相同数量的状态和符号,并压入新的状态和新的非终结符。
    在这里插入图片描述

在这里插入图片描述
这里X可以是非终结符
在这里插入图片描述

  • 二义性
    假设有二义性文法:
    在这里插入图片描述
    它对应LR(0)相集为:
    在这里插入图片描述
    假设输入为id + id * id ,处理完id+id 之后应该进入状态7. 这时发生了冲突,因为既可以选择规则 E − > E + E ⋅ E-> E+E\cdot E>E+E,产生动作规约也可以选择规则 E − > E ⋅ ∗ E E-> E\cdot*E E>EE产生动作移入。通常的做法是根据优先级手动规定此时应该发生的动作

AST抽象语法树

以下是一个生成语法树的语法制导的翻译方案

每个节点的类都是Node的子类,Node有两个直接子类Expr和Stmt。每一种运算符对应一种Stmt的子类,比如While,If.
Seq表示语句序列,也是Stmt的子类
在文法中,由stmt生成expr时,stmt的节点被赋予一个Stmt的子类Eval

符号表

在这里插入图片描述

  • block代表用大括号包围的语句块,每一个语句块有自己的作用域top
  • 如果在当前作用域找不到某符号,就会在上层的作用域中递归查找,每个作用域都保存了上层的指针
  • decl 表示声明

左值,右值,三地址码

a+a*(b-c)+(b-c)*d可以表示为DAG和三地址码:
在这里插入图片描述

可以在把表达式翻译成语法树的同时生成三地址码
左值表示三地址码中的储存位置,右值用来保存临时值
一个表达式expr既可以被当成左值也可以被当成右值,例如表达式a[i]=2a[j-k],对应的语法树的根节点是Assign节点,它的两个子节点是表达式a[i]和表达式2a[j-k]。a[i]需要被当成左值计算:
在这里插入图片描述
最后整个表达式的三地址码为

	t3 =  j- k
	t2 = a [t3 ]
	t1 = 2 * t2
	a [  i ] = t1

语法制导SDD

  • 依赖图描述了某个语法分析树中节点属性之间的信息流,从一个属性实例到另一个属性实例的边表示计算第二个属性实例时需要第一个属性实例的值

  • sdt (语法制导的翻译方案)是在产生式中嵌入程序片段,按先序遍历语法树时执行动作

  • inh表示继承属性,会依赖父节点或兄弟节点。syn表示综合节点,只依赖于子节点。

  • 把一个左递归的sdt改成非左递归:

    原sdt:
    在这里插入图片描述
    消除左递归后:
    在这里插入图片描述

    在这里插入图片描述
    这里b图中R的继承属性i可以看成是R子树左边所有字符串在a图中对应的根节点。

  • 如果sdd所有属性都是综合属性,那么它是s属性的。如果sdd中存在产生式A->X1X2X3…Xi, 并且Xi的继承属性只依赖A的继承属性或者Xi之前包括自己的综合和继承属性,则称这个sdd是L属性的。如果只包含综合属,成为s属性的sdd 。 只有s属性和sdt属性可以在LL语法分析和LR语法分析中实现。
    在这里插入图片描述
    上图中,把产生式2和4中的动作用非终结符号M2和M4代替,LR分析时,并不能确定是用M2->空 规约还是M4->空 规约。即以上不属于LR文法

  • 边扫描边生成(自上而下)
    在这里插入图片描述
    递归下降语法分析
    在这里插入图片描述

    这里C是条件语句,L1和L2是方便跳转的标号。L1是while语句的开头,L2是while的循环体。

    在这里插入图片描述

    做自顶向下分析的时候,栈除了临时保存终结符和非终结符外,还负责保存sdt的动作。非终结符的属性和动作中要用到的属性也保存在栈中。负责继承属性的动作记录保存在非终结符上方,负责综合属性的动作记录保存在下方。如上图所示,S刚展开的时候,C,Action,S1和综合属性对应动作的记录中,有些属性是未知的,而第一个Action中的snext属性已经从S继承过来了。当运行到第一个Action时,它后面的非终结符还没有展开,所以它会填充栈中一些未知的属性。

  • 边扫描边生成(自下而上)
    LL文法是LR文法的子集,而自下而上的边扫描边翻译只能应用于LL文法,且必须是L属性的SDD。
    不能用LR文法并且L属性的SDD的原因是,如果LR文法中存在产生式A->BC,当规约到B时,不能计算B的继承属性,因为此时还不能 确定把B和产生式A->BC关联起来。

    在这里插入图片描述
    把动作看成是一个,从空符号到非终结符M的规约。动作中用到的属性作为M的继承属性,得到的结果作为M综合属性
    把综合属性的计算放在非终结符本身的规约过程里。
    由于是LL文法,所以在LR分析的时候,每一个符号的产生路径都已经确定。所以在规约一个非终结符的时候一定可以在它之下找到继承属性的动作记录对应的非终结符。例如识别到while的时候,一定可以在它之下找到保存S.next的记录。规约C的时候一定可以在它之下找到M。

    在这里插入图片描述

    在这里插入图片描述

中间代码

  • 数组
    数组声明的翻译:
    在这里插入图片描述
    在这里插入图片描述

    数组以array(2,array(3,integer))的形式保存在符号表中。

    多维数组表达式的翻译:
    在这里插入图片描述
    首先是对最外层数组L->id [ E ]的规约, 从符号表中拿到id对应的数组记录array,并从中取出元素类型elem作为下一层数组的type。另外,根据elem的宽度和这一层表达式E的值计算最外层数组的偏移量。最后整个数组的偏移量就是每一层的偏移量之和。

  • 类型推导
    类型表达式可以是一个基本类型,也可以是将类型构造算子作用于类型表达式。array就是一个构造算子
    函数就是一个类型表达式,一元函数写作s->t。其中s和t可以为类型变量,即可以取值为任何类型大的变量。

    合一: 把两个类型表达式中的类型变量替换成相同的类型,如果相等,则说两个类型表达式是合一的。 类型检查就是判断是否合一。
    一个判断合一的例子:
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述
如果union(m,n)中n为变量,m为非变量,则一定要非变量作为合并后的代表结点。
list(a1)和a2必须以list(a1)为代表结点
上面的算法允许list(a1)和a1进行合一,实际上是错误的。

  • 控制流
    在这里插入图片描述
    在这里插入图片描述
    B.true和B.false表示B.code内部求出表达式值后需要跳转的标号。

如果布尔表达式用于赋值,则在真假出口分别将true和false赋值给新的临时变量t
例如x=a<b&&c<d可以解析为:
在这里插入图片描述
使用两趟式翻译S->if(B)S1时,需要先解析B再解析S1,所以B.true和B.false需要作为S的综合属性再继承给B。

使用回填技术
在这里插入图片描述
在这里插入图片描述
每个标号表示指令序列中一条三地址指令的序号。M的动作负责保存||后或&&后的下一条指令的标号。truelist和falselist是指令标号列表。例如标号100对应 if x <100 goto __ , truelist包含标号100,backpatch(truelist,102)相当于把100号指令填充为if x< 100 goto 102。nextinstr表示下一条指令的标号。

在对switch 翻译的时候,每识别到一个case就创建一个新的标号,并将入队列,识别完后再构造控制流。

运行时刻环境

被调用者的活动记录栈中,有top和top_sp。 top是当前真正的栈顶,而运行时刻栈top_s是机器状态字段的末端,其中保存着调用者的top_s,又称为控制链,方便恢复。而top的恢复需要调用者自己通过top_s减去参数和返回值以及及其状态等占用的字节。 局部变量保存在活动记录栈顶。
在这里插入图片描述
变长数组的内容并不在活动记录中,只有他们的开始位置在活动记录中。

在c语言中函数和全局变量一样分配在静态区,所以不允许嵌套声明函数。

ML是一个函数式语言,除了数组,变量的初始值不能修改。ML接受函数参数,并可以返回修改后的参数。ML中没有循环语句,因为变量不能修改,只能不断把循环变量i+1作为参数传给自身。
在这里插入图片描述
ML中定义作用域包括list中位于它之后的其它定义,和in到end之内的语句。定义只能在let 和 in之间。

在这里插入图片描述

访问链和控制链的区别是,控制链永远指向调用它的活动记录,而访问链不管调用它的是谁,指向的是嵌套定义它的函数的最靠近它的一个活动记录。

如果p是在q中定义的,那p的嵌套深度np=nq+1

在不考虑通过参数传递一二过程并调用的情况下,过程q调用p,(1)如果p的嵌套深度大于q,为了保证p在q的作用域内,p一定是在q中定义的,所以np=nq+1,q调用p时,将其访问链指针指向自己; (2) 如果深度相同,说明p和q是一起定义的,或者p=q。(3)如果np<nq, 说明p和q的访问链上有共同的祖先r,p是r的直接子节点,q是间接子节点,q调用p是通过nq-np+1步在访问链上找到r,并传递给p。注意,只有情况(1)可以使得栈顶活动记录的深度不断增加,所以可以肯定栈顶活动记录访问链上所有祖先此时都在栈上。
当允许过程以参数的形式被调用时,比如ML语言中的的例子:
在这里插入图片描述
在c中定义的函数d被传递个给b,并在b中被调用,此时传递者c肯定知道d的访问链,所以可以将其一并传递给b。因为这里参数不是以返回值的形式传递的,所以当b调用d时,c的活动记录一定还存在与栈中。

为了简化访问链,可以维护一个显示表,比如d[i]指向当前栈中最高的对应于深度i的活动记录。如果此时有一个新的深度为i的过程p被调用,就需要p保存d[i],并更新显示表中d[i]的值。等到p返回时再恢复旧的d[i]

当支持过程以返回值的形式被调用时,例如python,lua,不能保证访问链上的祖先一定有活动记录在栈上。在lua中,外部的局部变量称为upvalue。 当lua编译一个函数时,主要保存函数体对应的虚拟机指令、函数用到的常量值(数,文本字符串等等)和一些调试信息。当正真运行时,lua会为函数创建一个对象(闭包),包含相应函数原型的引用、环境(用来查找全局变量的表)的引用以及一个由所有upvalue引用组成的数组。当外部函数还在活跃时,upvalue引用指向栈上的upvalue。如果此时外部函数返回,lua就会为栈上的upvalue分配新的空间,并使得闭包内的upvalue引用指向他们。闭包内的引用还可以指向外部函数自己的upvalue引用,以支持多重嵌套。

堆管理和垃圾回收

  • 堆管理
    为了支持块合并,在每个块的左边界和右边界保存块起始地址和长度以及该块是否空闲,这两个相同的标记称为边界标记。边界标记的旁边保存链表需要用的属性(块被分配后这些属性就会被覆盖)。回收的时候可以根据左右标记合并相邻的空闲块,因为总是尽可能地合并,所以只用考虑被回收空闲块左边和右边的一个相邻块。合并后要更改空闲链表中的相应记录。

LLVM

在这里插入图片描述

前端负责把高级语言转换成LLVM IR , LLVM IR 可以通过一系列优化或者手工定义的规则改进代码,然后输入代码生成器生成目标机器码。

猜你喜欢

转载自blog.csdn.net/weixin_39849839/article/details/112141750