前言

虽然标题是程序语言的语法,但是讲的是对词法和语法的解析,其实关于这个前面那个写编译器系列的描述会更清楚,有关语言语法的部分应该是穿插在整个设计当中的,也看语言设计者的心情了

和英语汉语这些自然语言不一样,计算机语言必须是精确的,它们的语法和语义都必须保证没有歧义,这当然也让语法分析更加简单

所以对于编译器一项很重要的任务就是时别程序设计语言的结构规则,要完成这个目标就需要两个要求:

  • 完成对语法规则的描述
  • 确定给定程序是否按照这些规则构造起来,也就是符合语法规则

第一个要求主要由正则表达式和上下文无关文法来描述完成,而第二个要求就是由编译器来完成,也就是语法分析了

描述语法:正则表达式和上下文无关语法

对于词法,都可以用三种规则描述出来:

  1. 拼接
  2. 选择
  3. Kleene(也就是重复任意多次)

比如一个整数常量就可以是多个数字重复任意多次,也叫做正则语言。如果对于一个字符串,我们再加入递归定义即可以描述整个语法,就可以称作上下文无关语法

单词正则表达式

对于程序语言,单词的类型不外乎关键字、标识符、符合和各种类型的常量

对于整数常量就可以用这样的正则表达式来表示

integer -> digit digit* digit -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

上下文无关文法

一般正则表达式只适用于描述单词,因为正则表达式无法描述嵌套结构,一般正则表达式的实现都是用有限状态自动机,之前用Python实现了一个简单的正则表达式引擎也是这样,但是对于匹配任意深度的嵌套结构就需要有一个任意大的状态机,显然不符合。而定义嵌套结构对于描述语法非常有用,所以就有了上下文无关文法

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在送入语法分析器

有限状态自动机

使用有限状态机的词法分析一般都是这样的几个步骤

  • 给出词法的正则表达式

  • 将正则表达式转换为非确定有限自动机(NFA)

其实对于任意的正则表达式都可以用拼接、选择和Kleene闭包来表示

而同样的,有限自动机也可以通过这三种方式来表示,图就不画了,这个在之前写Python正则表达式引擎的文章里都画过了(溜了

  • 将NFA转换为确定性有限状态自动机(DFA)

将NFA转换到DFA可以采用的是子集构造法,主要思想就是,在读入给定输入之后所到达的DFA状态,表示的是原来NFA读入同样输入之后可能澳大的所有状态

  • 最小化DFA

对于最小化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自上而下

表格驱动的LL是基于一个语法分析表格和一个栈

分析流程是

  1. 初始化一个栈
  2. 将开始符号压入栈
  3. 弹出栈顶,然后根据栈顶的符号和当前的输入符号查表
  4. 如果弹出的是非终结符,将会继续查表来确定下一个压入栈中的产生式
  5. 如果是终结符将进行匹配

预测集合

从上面可以看出来最重要的就是那个语法分析表格了,语法分析表格其实就是根据当前输入字符对下一个产生式的预测,这里就要用到一个概念:预测集合,也就是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。

  • 对所有新生成的节点重复构建

最后对每个新生成的节点进行重复的构建,直到完成所有所有的状态节点的构建和跳转

小结

这一篇主要是提了对词法和语法的分析过程,因为想要结合语言设计和实践,更详细的应该去看前面的写一个编译器系列

PL真有意思(二):程序设计语言语法的更多相关文章

  1. PL真有意思(四):控制流

    前言 对大多数计算模型而言,顺序都是基本的东西,它确定了为完成所期望的某种工作,什么事情应该最先做,什么事应该随后做,我们可以将语言规定顺序的机制分为几个类别: 顺序执行 选择 迭代 过程抽象 递归 ...

  2. PL真有意思(一):引言

    前言 断断续续学编译原理到之前发过写一个编译器和正则表达式引擎系列文章也有一段时间了,然后最近看完PLP这本书,这本书应该算是入门书,但是对我这种半吊子收获很大.所以为了弥补最近学操作系统和接外包摸的 ...

  3. PL真有意思(五):数据类型

    前言 现在大多数程序设计语言中都有表达式和/或对象的类型概念.类型起着两种主要作用: 为许多操作提供了隐含的上下文信息,使程序员可以在许多情况下不必显示的描述这种上下文.比如int类型的两个对象相加就 ...

  4. PL真有意思(八):其它程序设计模型

    前言 在之前几篇我们讨论的语法.语义.命名.类型和抽象适用于所有语言.然而我们的注意力都主要集中在命令式语言上,现在这篇来看看其它范式的语言.函数式和逻辑式语言是最主要的非命令式语言. 函数式语言 命 ...

  5. PL真有意思(七):数据抽象和面向对象

    前言 在之前的名字.作用域那篇提到模块类型,它使程序员可以从一个给定抽象出发,通过实例化产生多个实例:再后面是类,它使程序员可以定义一族相关的抽象. 在这一篇里,我们会来看一下面向对象程序设计及其三个 ...

  6. PL真有意思(三):名字、作用域和约束

    前言 这两篇写了词法分析和语法分析,比较偏向实践.这一篇来看一下语言设计里一个比较重要的部分:名字.在大部分语言里,名字就是标识符,如果从抽象层面来看名字就是对更低一级的内存之类的概念的一层抽象.但是 ...

  7. PL真有意思(六):子程序和控制抽象

    前言 在之前我们把抽象定义为一种过程,程序员可以通过它将一个名字与一段可能很复杂的程序片段关联起来.抽象最大的意义就在于,我们可以从功能和用途的角度来考虑它,而不是实现. 在大多数程序设计语言中,子程 ...

  8. PL/SQL语言语法

    一.前言 SQL全称是"结构化查询语言(Structured Query Language)",而PL/SQL是过程语言(Procedure Language),是对SQL的扩展. ...

  9. C语言语法笔记 – 高级用法 指针数组 指针的指针 二维数组指针 结构体指针 链表 | IT宅.com

    原文:C语言语法笔记 – 高级用法 指针数组 指针的指针 二维数组指针 结构体指针 链表 | IT宅.com C语言语法笔记 – 高级用法 指针数组 指针的指针 二维数组指针 结构体指针 链表 | I ...

随机推荐

  1. Unity3D 通过JSON查询天气

    一.天气查询API 获取天气信息,首先要找到提供天气数据的接口,我使用的是高德地图免费为我们提供的,网址为 https://lbs.amap.com/api/webservice/guide/api/ ...

  2. TCC推导过程

    svn 账号 yuanzn 密码:TCH5mb 项目分层 MapperDao 数据校验 throw new CloudBaseRuntimeException Helper 数据转换 manager ...

  3. 造轮子了!NETCore跨平台UI框架,CPF

    CPF(暂时命名)(Cross platform framework),模仿WPF的框架,支持NETCore的跨平台UI框架,暂时不够完善,只用于测试,暂时只支持Windows和Mac.支持数据绑定, ...

  4. python编程系列---args与kwargs详解

    args与kwargs详解 """ Process([group [, target [, name [, args [, kwargs]]]]]) - target:目 ...

  5. LeetCode初级算法--设计问题01:Shuffle an Array (打乱数组)

    LeetCode初级算法--设计问题01:Shuffle an Array (打乱数组) 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:h ...

  6. 安装配置 Android Studio

    概述 Android Studio 本身应该是开箱即用的,但是由于 dl.google.com 访问太慢,导致了这样那样的问题,因此我们只需要改一下 hosts 就行了 具体步骤 在Ping检测网站查 ...

  7. 保存为txt

    打开对话框保存为txt #region this.dDownTable = (DataTable)(this.dg1.DataContext); ) { string fName = string.E ...

  8. 冷知识: 不会出现OutOfMemoryError的内存区域

    程序计数器(PC) 因为程序计数器只是记录当前线程正在执行的那条字节码指令的地址,即使出现死循环都不会内存溢出

  9. GStreamer基础教程11 - 与QT集成

    摘要 通常我们的播放引擎需要和GUI进行集成,在使用GStreamer时,GStreamre会负责媒体的播放及控制,GUI会负责处理用户的交互操作以及创建显示的窗口.本例中我们将结合QT介绍如何指定G ...

  10. 一次PHP代码上线遇到的问题

    exception ‘CDbException’ with message ‘The table “pms_goods” for active record class “PmsGoods” cann ...