虽然标题是程序语言的语法,可是讲的是对词法和语法的解析,其实关于这个前面那个写编译器系列的描述会更清楚,有关语言语法的部分应该是穿插在整个设计当中的,也看语言设计者的心情了git
和英语汉语这些天然语言不同,计算机语言必须是精确的,它们的语法和语义都必须保证没有歧义,这固然也让语法分析更加简单github
因此对于编译器一项很重要的任务就是时别程序设计语言的结构规则,要完成这个目标就须要两个要求:正则表达式
第一个要求主要由正则表达式和上下文无关文法来描述完成,而第二个要求就是由编译器来完成,也就是语法分析了markdown
对于词法,均可以用三种规则描述出来:闭包
好比一个整数常量就能够是多个数字重复任意屡次,也叫作正则语言。若是对于一个字符串,咱们再加入递归定义便可以描述整个语法,就能够称做上下文无关语法函数
对于程序语言,单词的类型不外乎关键字、标识符、符合和各类类型的常量oop
对于整数常量就能够用这样的正则表达式来表示spa
integer -> digit digit* digit -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
设计
通常正则表达式只适用于描述单词,由于正则表达式没法描述嵌套结构,通常正则表达式的实现都是用有限状态自动机,以前用Python实现了一个简单的正则表达式引擎也是这样,可是对于匹配任意深度的嵌套结构就须要有一个任意大的状态机,显然不符合。而定义嵌套结构对于描述语法很是有用,因此就有了上下文无关文法code
expr := id | number | - expr | ( expr ) | expr or expr
op := + | - | * | /
对于上下文无关文法,每条规则叫作一个产生式,产生式左部的符合称为非终结符,而右部则是多个终结符或者非终结符,最后全部规则都会推到至终结符上,而终结符就是正则表达式定义的单词
一个正确的上下文无关文法,就能够指导咱们如何生成一个合乎语法的终结符串
最简单的就是从开始符号开始,用这个产生式的右部取代开始符合,再从获得的串选择一个非终结符继续进行推导,直到没有剩下的非终结符,这个过程就像递归构造一个树的过程
expr := expr op expr
:= expr op id
:= expr + id
:= expr op expr + id
:= expr op id + id
:= expr * id + id
:= id * id + id
复制代码
可是对于给定的上下文语法有可能会推导出不止一颗语法分析树,咱们就说这个上下文语法是存在歧义性的。因此对于上面的上下文无关语法还有更好的文法
扫描也就是词法分析,词法分析彻底能够不须要什么正则表达式、自动机什么的,徒手撸出来,如今业界为了更好的生成错误信息,应该不少也是手工的词法分析器
手工的词法分析器,无非就是一直读入字符,到能判断出它的token在送入语法分析器
使用有限状态机的词法分析通常都是这样的几个步骤
其实对于任意的正则表达式均可以用拼接、选择和Kleene闭包来表示
而一样的,有限自动机也能够经过这三种方式来表示,图就不画了,这个在以前写Python正则表达式引擎的文章里都画过了(溜了
将NFA转换到DFA能够采用的是子集构造法,主要思想就是,在读入给定输入以后所到达的DFA状态,表示的是原来NFA读入一样输入以后可能澳大的全部状态
对于最小化DFA的主要思想是,咱们把DFA全部状态分为两个等价类,终止态状态和非终止状态。而后咱们就反复搜索等价类X和输入符合c,使得当给定C做为输入时,X的状态能转换到位于k>1个不一样等价类中的状态。以后咱们就把X划分为k个类,使得类中全部转台对于C都会转移到同一个老类的成员。直到没法再按这种方式找到划分的类时,咱们就完成了
这四个步骤在以前的写的正则表达式引擎中都完成了,在那三篇文章里会更详细一点
通常语法分析器的输入是token流,而输出是一颗语法分析树。其中分析方法通常能够分为自上而下和自下而上两类,这些类中最重要的两个分别称为LL和LR
LL表示从左向右,最左推导,LR表示从左向右,最右推导。这两类文法都是从左到右的顺序读取输入,而后语法分析器试图找出输入的推导结果
通常自上而下的语法分析器比较符合以前的推导方法,从根节点开始像叶节点反复的递归推导,直到当前的叶节点都是终结符
递归降低很符合上面说的从根节点出发进行推导,通常用于一些相对简单一些的语言
read A read B sum := A + B write sum write sum / 2 复制代码
好比对于这个程序的递归降低,语法分析器一开始调用program函数,在读入第一个单词read后,program将调用stmt_list,再接着调用stmt才真正开始匹配read A。以这种方式继续下去,语法分析器执行路径将追溯出语法分析树的从左向右、自上而下的遍历
表格驱动的LL是基于一个语法分析表格和一个栈
分析流程是
预测集合
从上面能够看出来最重要的就是那个语法分析表格了,语法分析表格其实就是根据当前输入字符对下一个产生式的预测,这里就要用到一个概念:预测集合,也就是First和Follow集合。这个在以前写编译器系列讲的比较详细,在这里就不写了
固然LL语法也会有不少处理不了的文法,因此也才会有其它的语法分析方法
在实践中,自下而上的语法分析都是表格驱动的,这种分析器在一个栈中保存全部部分完成的子树的根。当它从扫描器中获得一个新的单词时,就会将这个单词移入栈。当它发现位于栈顶的若干符号组成一个右部时,它就会将这些符号归约到对应的左部。
一个自底向上的语法分析过程对应为一个输入串构造语法分析书的过程,它从叶子节点开始,经过shift和reduce操做逐渐向上到达根节点
自底向上的语法分析须要一个堆栈来存放解析的符号,例如对于以下语法:
0. statement -> expr 1. expr -> expr + factor 2. | factor 3. factor -> ( expr ) 4. | NUM 复制代码
来解析1+2
stack | input | |
---|---|---|
null | 1 + 2 | |
NUM | + 2 | 开始读入一个字符,并把对应的token放入解析堆栈,称为shift操做 |
factor | + 2 | 根据语法推导式,factor -> NUM,将NUM出栈,factor入栈,这个操做称为reduce |
expr | + 2 | 这里继续作reduce操做,可是因为语法推导式有两个产生式,因此须要向前看一个符合才能判断是进行shift仍是reduce,也就是语法解析的LA |
expr + | 2 | shift操做 |
expr + NUM | null | shift操做 |
expr + factor | null | 根据fator的产生式进行reduce |
expr | null | reduce操做 |
statement | null | reduce操做 |
此时规约到开始符号,而且输入串也为空,表明语法解析成功
0. s -> e 1. e -> e + t 2. e -> t 3. t -> t * f 4. t -> f 5. f -> ( e ) 6. f -> NUM 复制代码
先在起始产生式->右边加上一个.
s -> .e
复制代码
对.右边的符号作闭包操做,也就是说若是 . 右边的符号是一个非终结符,那么确定有某个表达式,->左边是该非终结符,把这些表达式添加进来
s -> . e
e -> . e + t
e -> . t
复制代码
对新添加进来的推导式反复重复这个操做,直到全部推导式->右边是非终结符的那个所在推导式都引入
把 . 右边拥有相同非终结符的表达式划入一个分区,好比
e -> t .
t -> t . * f
复制代码
就做为同一个分区。最后把每一个分区中的表达式中的 . 右移动一位,造成新的状态节点
根据每一个节点 . 左边的符号来判断输入什么字符来跳入该节点
好比, . 左边的符号是 t, 因此当状态机处于状态0时,输入时 t 时, 跳转到状态1。
最后对每一个新生成的节点进行重复的构建,直到完成全部全部的状态节点的构建和跳转
这一篇主要是提了对词法和语法的分析过程,由于想要结合语言设计和实践,更详细的应该去看前面的写一个编译器系列