斯坦福-CS143-编译原理中文笔记-一-

80 阅读1小时+

斯坦福 CS143 编译原理中文笔记(一)

P1:p01 01-01-_Introduction - 加加zero - BV1Mb42177J7

欢迎这门编译器课程,我叫亚历克斯·艾肯,我是斯坦福大学的教授,我们将讨论编程语言的实现。

实现编程语言有两种主要方法,编译器和解释器,这门课主要讲编译器,但我想在第一节课说几句解释器,解释器擅长做什么,我会画张图,这个盒子是解释器,它接收,让我用大字标上,它以输入接收,你的程序,你编写的。

以及你想运行的程序的数据,并直接产生输出,这意味着它不处理程序,在执行输入之前,所以你只需编写程序,然后运行解释器处理数据,程序立即开始运行,因此可以说解释器是实时的,意味着它的工作是运行程序的一部分。

编译器结构不同,我们可以在这里画个图,用一个大写的C标记编译器,编译器以程序为输入,仅此而已,然后生成可执行文件,此可执行文件可能是汇编语言,可能是字节码,可能是多种实现语言的其中之一。

但现在可单独运行于数据上,这将产生输出,好的,嗯,在这种结构中,嗯,编译器离线,意味着我们先预处理程序,编译器本质上是一个预处理步骤,产生可执行文件,然后我们可以在多个,不同输入上运行相同的可执行文件。

无需重新编译或对程序进行其他处理。

我认为,稍微介绍一下编译器和解释器,最初是如何开发的有帮助,故事始于20世纪50年代,特别是IBM制造的704机器,这是他们的首台商用成功机,尽管之前曾尝试过一些早期机器,但无论如何。

关于704有趣的是,一旦客户开始购买和使用它,他们发现软件成本,超过了硬件成本,而且不仅仅是一点点,而是很多,这很重要,因为这些,当年硬件极其昂贵,即使那时硬件绝对昂贵,再也不会那么贵了,已经。

软件是充分利用计算机的主要成本,这促使许多人,思考如何更好地编写软件,如何提高编程生产力。

提高编程生产力的最早尝试之一称为快速编码,153年由约翰·巴库斯开发,现在速编可视为早期解释器示例,和所有解释器一样,它有优缺点,主要优点是程序开发快,因此程序员效率更高,但缺点也不少。

速编程序代码比手写慢10-20倍,今天解释器程序也如此,若使用解释器实现,通常比编译或手写慢,速度码解释器占300字节,这看起来并不多,实际上,今天300字节像极小程序,但在那时。

要记住这是机器内存的30%,这是74整个内存的30%,解释器占的空间成关注点,速度编码未流行,但约翰·巴库斯认为有前途,他有了另一个项目想法,当时最重要的应用是科学计算,程序员认为。

以机器可执行的形式写下公式,约翰认为,速度编码的问题在于公式实际上被解释,他认为,如果首先将公式翻译成机器可直接执行的形式,代码将更快,同时仍允许程序员以高级别编写程序。

于是公式翻译项目或FORTRAN项目诞生了,Fortran运行于1954至1957,有趣的是,他们以为只需1年建编译器,但最终花了3年,就像今天,他们并不擅长预测软件项目时长。

但到1958年是个成功项目,超过50%的代码是Fortran,50%的程序是Fortran,对新技术的迅速采纳,今天有这种成功我们很满意,当然那时他们欣喜若狂,当时人们认为FORTRAN提高了抽象水平。

提高了程序员生产力,并让每个人更好地利用这些机器。

因此FORTRAN 1是第一个成功的高级语言,它对计算机科学产生了巨大影响,特别是它导致了大量理论工作,实际上关于编程语言有趣的是理论和实践的结合,因为在编程语言中很难做好工作,没有扎实理论和工程技能。

很难构建优秀系统,编程语言中有大量优秀系统构建材料,通常涉及微妙且富有成效的理论交互,我认为这是该领域最吸引人的地方之一,作为计算机科学学科的研究主题,Fortran的影响不仅限于计算机科学研究,当然。

还促进了实用编译器的开发,事实上,其影响深远至今,现代编译器仍保留四阶段。

那么FORTRAN 1的结构是?它由五个阶段组成,嗯,词法分析和语法分析,共同处理语言语法方面,哪个,当然,负责更多语义方面,类型和范围等,规则,优化,程序的转换集合,以加快运行或减少内存使用。

最后是代码生成,实际进行到另一种语言的翻译,根据目标,翻译可能是机器码,可能是虚拟机的字节码,甚至可能是另一种高级编程语言,好的,本次讲座就到这里。

P10:p10 03-04-_Formal_Languages - 加加zero - BV1Mb42177J7

欢迎回到本视频,我们将稍作偏离,讨论形式语言,形式语言在理论计算机科学中扮演重要角色,但在编译器中也同样重要,因为在编译器内部,我们通常操纵几种不同的形式语言。

我们的正则表达式是一个形式语言的示例,但实际上有帮助,我认为,在理解正则语言以及稍后看到的所有形式语言时。

让我们从定义开始,正式语言有一个字母表,所以一些字母集sigma,然后,该字母表上的语言只是从字母表中抽取字符的字符串集,在正规语言的情况下,我们有某些构建字符字符串集的方法。

但其他类型的语言会有不同的字符串集,一般来说,正式语言就是某个字母表上的任何字符串集。

你熟悉的语言的一个例子,是来自英语字符字母表的表格,这些是英语句子集合,这不是一个正式语言,因为我们可能对哪些,英语字符序列是,实际上有效的英语句子有分歧,但我们可以想象定义一些规则。

说某些字符串是英语句子,其他的不是,如果我们能达成一致,这将是一个完全正式的语言,更严格的正式语言如下,我们可以选择ASCII字符集作为字母表,语言为所有有效C程序的集合,这绝对是一个非常明确的语言。

这正是C编译器将接受的输入集,我想在这里强调的对比是,字母表实际上很有趣,不同的正式语言,字母表非常不同,我们无法真正谈论正式语言,或我们感兴趣的字符串集,首先定义字母表。

形式语言的重要概念,通常我们有一个语言中的字符串,我们称之为表达式e,表达式e本身只是语法的一部分,它是一个,某种意义上是程序,或代表我们感兴趣的其他东西,因此我们有一个函数l。

它将语言中的字符串映射到它们的含义,因此,例如,关于正则表达式的情况,嗯,这将是正则表达式,并且将被映射到一个字符串集,该正则表达式表示的正规语言,我们上次看到了我们为正则表达式写出的意义函数的例子。

所以让我们以正则表达式为例,我将首先写下正则表达式的意义,我上次视频中写下的方式,所以如果你记得我们有正则表达式epsilon,它表示包含仅一个字符串的集合,即空字符串,然后对于字母表中的每个字符c。

我们有一个正则表达式c,它也表示包含仅一个字符串的集合,即单个字符c,然后我们有一堆复合表达式,例如,嗯,A加b,等于集合a和b的并集,然后我们有连接,所以我可以并列a和b。

等于从每个集合中选择一个字符串的笛卡尔积,并将它们连接在一起,最后有迭代,所以我可以写a星,等于对i大于零的所有集合a的i次方的并集,正确,嗯,并且有趣的是这个定义,你可以看到我们正在映射。

在这里我们有表达式,让我在这里换个颜色,在这里我们有表达式,而在这里我们有集合,但这里有些奇怪并且不太正确,因为你可以看到在这里显然我们有一个表达式,我们有一个语法片段a加b,然后不知何故在另一边。

这些a和这个a和这个b神奇地变成了集合,我们正在取并集,同样在这里我们从这个集合中选择一个元素,但这个集合也是一个表达式,那意味着什么,我们以某种方式混淆了集合和表达式,这就是意义函数旨在修复的。

这就是它们旨在澄清的,所以实际上我们想说的是存在某种映射,所以l映射从表达式到字符串集epsilon,所以l将表达式映射到字符串集,因此,我们真正想要说的是l(epsilon)是这个集合,好的。

这是一个映射函数,如果你以前没见过这种表示法,这是描述函数的标准表示法,它只是说l是一个从域内元素到域外元素的函数,好的,同样,这个表达式的语言是这组,这变得非常有用对于复合表达式,因为我们听说我们说。

这个表达式的语言等于a和b语言的并集,现在你可以看到递归了,我们使用l解释a和b,然后取结果并集,好的,所以现在清楚什么是集合,什么是表达式了,类似地,这里a与b连接的语言。

我们将从这两个表达式的语言中选择元素,然后从这两个集合中形成另一个集合,最后对于迭代,a星号的语言等于对a的一堆表达式的意义的并集,a的i次方是一个表达式,这是一段语法。

我们必须将其转换为集合才能取并集,所以现在这是正规表达式意义的正确定义,其中我们明确说明了意义函数l,并且我们确切地展示了如何递归地,应用l,将复合表达式分解为简单的表达式,我们计算其意义。

然后计算集合,呃,从那些,嗯,呃,从那些单独的,呃,较小的集合。

所以使用意义函数有几个原因,呃,我们刚刚看到其中一个,那就是明确,在我们的定义中什么是语法,什么是语义,定义中的一些部分是表达式,而一些部分是,意义或集合,使用l使清楚l的参数是程序或表达式。

结果就是集合,输出是集合,但还有其他几个原因将语法和语义分开,一个是它允许我们将表示法作为一个单独的问题来考虑,也就是说,如果我们有语法和语义是不同的,我们可以改变语法,同时保持语义不变。

我们可能会发现某些语法比其他语法更好,对于我们所感兴趣的问题,或对于我们所感兴趣的语言,另一个分离两者的原因是,因为表达式和含义,因为语法和语义不是一一对应的。

实际上我在之前的视频中用正则表达式说明了这一点,但我想在这里重申,通常有更多表达式,嗯,而不是含义,这意味着可能有多种方式,嗯,写一个表达式。

意味着同样的事情,我想花点时间说明,为什么分离语法和语义对表示法有益,所以每个人都熟悉我们的数字系统,所以我可以写数字,如0、1、42和107,有很好的算法来描述如何添加,和减法和乘法这样的数字。

但也有一些旧式的数字表示法,比如罗马数字,所以我可以有这个数字1,我可以有这个数字4,数字10,比如说数字40,我想是这样写的,和这个数字系统的问题,首先,让我强调一下,这两个有相同的含义。

所以这些语言中的表达式的含义是,是整数,在这个语言中也是完全一样的,所以,这个想法,这些两个系统的含义仅仅是数字,但表示法非常不同,用罗马数字写的数字看起来完全不同,用阿拉伯数字写的数字,事实上。

罗马数字真的很痛苦,来做加法、减法和乘法,事实上,在古代,当这是一个常见系统时,并不十分清楚如何去做,而且很少有人真正擅长用,嗯,用这个系统做算术,因为算法有点复杂,当我们后来转向阿拉伯系统时。

这是一个很大的改进,因为人们,更容易学习如何用这些数字做基本算术,唯一改变的是一种表示系统,因此,表示法非常重要,因为它决定了你如何思考,它决定了你能说的内容,以及你将使用的程序。

所以不要低估符号的重要性,这是分离语法和语义的一个原因,因为我们可以留下我们想要做的想法,数字本身,并尝试不同的表示方式,我们可能会发现某些方式比其他方式更好,我给出的分离语法和语义的第三个原因是。

在许多有趣的语言中,多个表达式,多个语法片段将具有相同的语义,现在回到正则表达式,让我们考虑正则表达式零星,这是所有零字符串的语言,所以任何长度的零字符串,例如,我也可以这样写。

另一种写法是epsilon加零零星,你可以看到,这个表达式是所有零字符串,长度至少为一,我们从epsilon得到空字符串,所以这等于零星,它们只是,你知道,任何这些组合也将构成等效的语言,例如那个等等。

实际上有无数种方式,我可以写这个语言,但所有这些都完全一样,如果你考虑一下,这意味着通常,如果我以不同的方式绘制这两个域,我在这里考虑不同的表达式和不同的明确含义,映射它们之间的函数l。

函数l是多对一的,所以你知道,这个空间中有一些点,许多不同的表达式或语法片段映射到相同的含义,这是有趣形式语言的一般特征,这在编译器中实际上非常重要,因为这就是优化的基础。

有许多不同的程序实际上功能等效,这就是我们可以用运行速度更快的程序替换另一个程序的原因,这就是我们可以用另一个程序替换一个程序的原因,如果它运行得更快并且做完全相同的事情,所以我们不能做优化,你知道。

我们能够进行优化的原因正是意义函数是多对一的,意义是多对一的,记住这里的重要一点是它永远不会是一对多,我们不想要相反的情况,若情况相反,若我能将一点映射到两个不同含义,嗯,首先,这不再是一个函数。

并且意味着某些表达的含义,比如我们编程语言中的,并不明确,当你编写程序时,实际上含义模糊,它意味着这个或那个,这不是我们喜欢的状况,因此我们期望意义函数对于非平凡语言是多对一的,我们不想它们成为一对多。

今天视频结束,下次我们将继续讨论词法分析。

P11:p11 03-05-_Lexical_Specific - 加加zero - BV1Mb42177J7

欢迎回到本视频,将展示如何使用正则表达式指定编程语言的不同方面。

从关键字开始,这是一个相对简单的案例,仅对3个关键字进行操作,为if else或then编写正则表达式,如何为更多关键字编写将显而易见,为if编写正则表达式,即为i的正则表达式,后跟f的正则表达式。

这是这两个正则表达式的连接,然后将与else的正则表达式联合,那是什么?else由4个单独字符组成,因此必须写出这4个字符的连接,如您所见,这有点冗长,所有这些引号和混乱的阅读,实际上有一种常用的简写。

现在让我切换到那个,若要编写单个字符序列的正则表达式,只需在序列最外层字符周围放置引号,例如,大多数工具将允许您编写此内容,我在开头放置引号,我写i f然后写闭引号,这完全等同于这个。

这是两个单个字符正则表达式的连接,类似地对于else,类似地对于them,如果有更多关键字,只需将它们全部写出并联合在一起,现在考虑一个稍微更复杂的例子,让我们思考如何指定整数。

我们希望它们是数字的非空字符串,这里第一个问题是写出数字是什么,这相当直接,数字只是0到9的单个字符中的任何一个,我们已经知道如何编写单个字符的正则表达式,只需将这10个指定此的联合起来,只需片刻,嗯。

完成那里我们走,这是一个对应所有单个数字字符串的正则表达式集合,因为我们将不时地引用它,并且因为这是一个非常常见的事情想要做,大多数工具都有命名正则表达式的功能,例如,我可以将其命名为digit。

单个数字是任何由这个正则表达式生成的或属于该集合的,现在我们要做的是多个数字,好吧,我们知道如何做到这一点,我们知道如何做到这一点,我们可以遍历,嗯,这个,任意多次的单数,因此我们得到所有字符串。

所有可能的数字字符串,这非常接近我们想要的,除了我们想要的字符串不能为空,我们不把空字符串算作整数,这是一个简单的解决方法,我们只需说整个序列必须以一个数字开始,然后跟着是,零个或更多附加数字。

再次重申,我们说必须至少有一个数字,然后跟着零个或更多附加数字,这种模式对于给定的语言非常常见,所以如果我想说我至少有一个a,我写成a a星,因为这部分说零个或更多,第二部分说零个或更多a。

而第一部分说必须至少有一个a,因为这种非常常见,有一个简写,我认为被所有正则表达式处理器支持,那就是写a加,a加就是a星的简写,因此我们可以稍微简化这个正则表达式,写成digit加。

现在让我们看另一个例子,比之前的更复杂,让我们想想如何定义标识符,它们是以字母开始的字母或数字字符串,所以我们已经知道如何做数字,所以让我们暂时关注字母,那么如何写出字母的正则表达式,我们得命名它。

所以我们会说字母实际上是一个字母,现在我们必须写出所有单个字母的正则表达式,你知道,直接,但繁琐,我们必须说小写a小写b,小写c,小写d,正如你所见,这将是一个非常长的正则表达式。

我们将有26个小写字母和26个大写字母,整个东西写下来会很繁琐,所以实际上我们不做这个,相反,让我提一个简写,工具支持编写这种正则表达式,更简单,称为字符范围,方括号内可写字符范围,如何做到?

我有起始字符和结束字符,用横线分隔,意味着从第一个字符到第二个字符的所有单字符正则表达式的并集,从第一个字符开始,到第二个字符结束,中间所有内容,这就是所有小写字母的正则表达式。

然后我可以有另一个字符范围,在同一方括号内包含所有大写字母,所以大写A到,Z 好的,这个,嗯,右侧的正则表达式确切定义了我不想写出的并集,这给了我们一个字母的定义,现在我们状况很好。

我们已经有一个数字的定义,我们现在已经有了一个字母的定义,因此我们可以写出余下的定义,因此我们希望整个正则表达式始终以字母开头,好的,因此标识符始终以字母开头,之后它可以是一个字母或数字的字符串,好的。

因此或表示将有一个并集,在第一个字母之后,我们可以有一个字母或数字,然后我们可以有这些事物的任意字符串,因此我们在整个东西上放一个星号,这就是标识符的定义,以单个字母开始,后跟零个或多个字母和数字。

由于我们在做完整的词法规范,我们还需要处理甚至字符串中,呃,我们不真正感兴趣的,部分。我们至少需要有一个规范,以便我们可以识别并丢弃它们,特别是,我们必须能够识别空格,我们将只取空格,非空空白序列。

换行和制表符,尽管还有其他空白字符,看起来可能像擦除,取决于你的键盘,可能还有其他,但这三个足以说明所有要点,所以你知道空白很容易写,那就是单引号内的空白,但换行和制表符有问题,因为新行。

文件中的回车有特殊含义,通常你知道,是的,你在行上,在这些正则表达式工具中,结束你正在工作的任何命令,词法工具,你知道,制表符也不容易写下来,在很多情况下,它与空白看起来没有太大不同,我们使用的工具有。

它们为这些提供单独名称,通常通过某种转义字符完成,反斜杠是最常用的,后跟字符名称,反斜杠n通常用于换行,反斜杠t通常用于制表符,我想强调,我的意思是,举这个例子的原因是为了说明,那。

我们需要一种命名字符的方式,它们实际上并没有很好的打印表示,还有其他字符甚至根本没有真正的打印表示,我们仍然需要在正则表达式中谈论它们,因为它们可能嵌入在文件中,我们稍后需要进行词法分析,所以无论如何。

这样做的方法是提供一种单独的命名方案,对于这些不可打印字符,通常使用转义序列,以特殊字符如反斜杠开始,后跟字符名称,如n为换行,这种情况下,和t为轻敲,因此完成定义,这给我们,你知道,一个字符空格。

然后需要一个非空序列,所以我们用括号,把整个联盟括起来,加个加号,这就得到我们想要的,让我们暂停讨论编程语言,看看使用正则表达式的另一个例子,嗯,从不同领域,这里有一个电子邮件地址。

结果电子邮件地址构成正规语言,世界上每个电子邮件处理设备,所以你的邮件程序和邮件服务器,都会进行正则表达式处理以理解电子邮件地址的含义,在发送的电子邮件中,因此。

我们可以将电子邮件地址视为由四个不同的字符串组成,由标点符号分隔,有一个用户名,然后是域名的三个部分,好的,为了简单起见,假设字符串仅由字母组成,实际上,它们可以由其他类型的字符组成,但让我们保持简单。

我们可以使用正则表达式写出更复杂的东西,但结构将保持不变,如果我们只考虑它们由字母组成,然后这四个字符串由标点符号分隔,所以这是@符号,和两个小数点,形成字符串的分隔符,因此,这是一个相对简单的东西。

可以编写正则表达式,基于我们所知道的,因此,用户名将是非空的字母序列,然后将被@符号跟随,然后域名的第一部分,也会是一个非空字母序列后跟一个点,然后其余部分就一样了,好的,所以这里,非常简洁地,嗯。

我们定义了一个大的电子邮件地址家族,如我所说,嗯,实际上,电子邮件地址稍复杂,但可用稍复杂的正则表达式写出。

最后,看最后一个例子,让我们看一个真实编程语言的词法规范片段,在这种情况下,语言是pascal,属于algal语言家族,Pascal是类型化语言的早期例子,与fortran和c属于同一大家族。

此pascal片段处理数字的定义,让我们看看这里,我将从底部开始,数字的整体定义是。嗯,三件事,呃,一些数字和可选分数,和可选指数,所以我们在处理浮点数,所以浮点数,我可以有一堆数字。

可能后跟指数或不跟,和想法,尽管仅从此定义看不到,分数或指数可独立存在,现在让我们自下而上简要工作,检查数字是否如我们所期望,单个数字是,所有常见数字的并集,正如我们所希望,非空数字序列是数字加。

这是我们已见过的,现在有趣的部分是看如何定义可选分数和可选指数,可选分数看起来不那么可怕,让我们先做那个,分数内部发生了什么,如果有小数分数,将有一个小数点,后跟一串数字,这就是浮点数的分数部分。

是十进制点后的东西,为什么加epsilon在外面,嗯,记住加是并集,epsilon代表空字符串,所以这说的是,数字的小数部分是存在的,或完全不存在,这就是如何说,某物是可选的,写出该事物的正则表达式。

然后在末尾做加epsilon,这意味着,我之前说的所有内容都可以在那里,或什么也没有,可选指数的结构类似,但更复杂,你可以看到整个指数是可选,因为这里有一些正则表达式,嗯,那是与epsilon的并集。

所以要么有东西,这是可选的x,这是指数部分,或根本不存在,现在让我们看看指数是如何定义的,如果它在,所以指数总是以e开始,所以这是指数,你知道标准指数表示法,并且它总是有一个非空字符串的数字。

所以有e后跟数字,中间可有可选符号,我们知道符号可选,因为epsilon是可能之一,整个,呃,整个符号可能缺失,那么符号的可能有哪些,可以是负数,或正数,所以可有正负号,或无符号,在这种情况下。

可能解释为正数,用加epsilon表示可选的惯用法很常见,因此许多工具提供另一种简写,另一种常见写法是,这是小数部分,然后可能缺失,正则表达式后的问号表示这个结构,我们或它与epsilon。

这个正则表达式较复杂,有两个可选部分,所以让我们写出来,我们有,呃,指数以e开始,然后呃,正负sin是可选,所以加问号,呃,后跟非空数字串,然后整个,呃是可选,整个指数可选,这是另一种更紧凑的写法。

总结。

我希望在这视频中说服你,正则表达式能描述许多有用语言,呃,我们见过编程语言片段,但电子邮件地址也可这样指定,嗯,其他如电话号码和文件名也是正则语言,日常生活中还有许多其他例子。

正则语言用于描述简单字符串集,并且,我还想强调,到目前为止我们使用正则语言作为语言规范,用它定义我们感兴趣字符串集,但还没说如何实现词法分析,仍需实现,将在后续视频中讨论,特别是,特别是。

将研究给定字符串s和正则表达式r,如何判断字符串是否属于正则表达式的语言。

R。

P12:p12 04-01-_Lexical_Specific - 加加zero - BV1Mb42177J7

欢迎回到本视频。

将讨论正则表达式如何用于构建,编程语言的完整词法规范。

在我们开始之前,我想快速总结一下正则表达式的符号,我们上次讨论的简写之一是a+,意味着至少一个a序列,或语言a a*,有时你会看到一个垂直线,呃,用于代替并集,因此a+b,嗯,也可以写成a|b。

可选的a是正则表达式a+ε的缩写,然后我们有字符范围,允许我们按顺序进行大量字符的并集,然后还有一个实际上非常重要,但我们上次没有讨论的是字符范围的补集,所以这个表达式意味着任何字符。

除了a到z的字符,在上次讲座中,我们讨论了以下谓词的规范,给定一个字符串,S是否属于语言,这是函数l的正则表达式,即我们定义了正则表达式的语言,并以字符串集合的形式讨论了它们的语义。

因此对于任何给定的正则表达式,我们可以手动推理,给定字符串是否属于该语言,但这并不足以满足我们想要做的事情,只是为了回顾,我们想要做什么,我们给定的输入,是一堆字符,所以这是一个字符字符串。

它可以比七个字符长得多,而我们真正想要做的是分割这个字符串,我们想在这个字符串中划线,将语言中的单词分开,当然,每个单词都应该是某个正则表达式的语言,但仅仅有一个是,否答案并不完全相同。

与有一种方法将字符串分割为其组成部分,因此我们必须将正则表达式适应这个问题,并且这将需要一些小的扩展,这就是本视频的全部内容。

那么让我们谈谈如何做到这一点,当我们想要设计语言的词法规范时,我们必须为每个标记类的词素编写正则表达式,我们上次讨论了如何做到这一点,我们已经讨论了如何做到这一点,对于数字。

我们可能使用数字加作为正则表达式,我们可能有一个关键词类别,即语言中所有关键词的列表,我们可能会有标识符类别,这是我们上次讨论的定义,以字母开始的字母或数字序列,然后我们会有一堆标点符号,比如左括号。

右括号,等,因此我们写下了一堆正则表达式,每个语言语法类别的1个。

现在这是我们的词法规范起点,作为第二步我们要做的是,我们将构建一个巨大的正则表达式,它只匹配所有令牌的词法场景,这仅仅是上一页上写下的所有正则表达式的并集,我们只需取所有这些正则表达式的并集。

这形成了语言的词法规范,我们会写出这个,我们并不真正关心这些正则表达式是什么,它们只是一些拇指规则,R1,R2等等,整个东西我们将称为r。

现在这里是如何,我们实际上使用这个词法规范进行词法分析的核心,因此让我们考虑一个输入,输入是一个字符串x1到xn,现在对于该输入的每个前缀,好的,我们将检查它是否属于正则表达式的语言。

因此我们将查看带有第一个字符的前缀字符串,我们将问自己,它是否属于这个大正则表达式的语言,如果它在如果它在语言中,那么我们特别知道该前缀是其中一个组成部分正则表达式的语言。

因为记住r等于所有不同令牌类别的和,因此我们知道这个前缀x1到xi属于某个rj的语言,好的,因此我们知道那是一个词,在我们的语言中,是我们在乎的其中一个令牌类别,因此我们简单地删除那个前缀从输入中。

然后回到3并重复,以这种方式我们不断地咬下输入的前缀,我们将这样做直到字符串为空,然后我们就电子方式分析了整个程序。

现在该算法有几个歧义,有一些东西没有明确说明,这些实际上是很有趣的,因此让我们花一点时间谈谈那些,第一个问题是实际使用了多少输入,所以让我们考虑以下情况,假设我们有x1到,Xi属于词汇规范的语言。

并且假设有一个不同的前缀,也在词汇规范的语言中,当然你的i不等于j,那看起来像什么,看起来像这样的一种情况,我们会有输入字符串,我们有两个不同的输入前缀,都是有效的标记类,问题是我们要哪一个。

你知道为了具体起见,给出一个具体的例子,让我们考虑当双等号在输入的开头时会发生什么,所以如果我们切掉了其他一些输入,你知道也许我们有了这个子串或这个输入的前缀,嗯,我们正在看,问题是,你知道。

这应该被视为一个等于号,嗯,这将是大多数语言中的赋值操作符,或者会被视为双等号,这在某些语言中是比较操作符,这是一个我们之前看过并讨论过的例子,实际上有一个明确的答案,我们应该总是选择较长的那个。

这有一个名字,它被称为最大贪婪,所以规则是,当面临两个不同的输入前缀的选择时,任何一个都是有效的标记,我们总是应该选择较长的那个,原因就是这样是人类自己阅读的方式,所以当我们看到一个双等号。

我们不会看到两个不同的等号操作符,我们看到的是一个双等号,如果我看,你知道我写在这里的这句话,你知道,当我们看h o w,我们不会看到三个字母,我们把这些都聚集在一起成为一个长的标记,我们尽可能走远。

直到我们看到一个分隔符,因为这就是人类工作的原理,我们让工具以同样的方式工作,这通常或几乎总是做正确的事,第二个问题是如果多个标记匹配,应该使用哪个标记,所以我的意思是什么,身体又好了。

我们有输入的前缀,并且它在词汇规范的语言中,记住词汇规范本身,也是由许多正则表达式组成的标记类联盟,由于,由于这个前缀在词汇规范的语言中,这意味着它,它必须属于某个特定的标记类,Rj。

但没说它不在完全不同标记的语言中,意味着这同一字符串可解释为两种不同标记之一,问题是如果发生这种情况,我们应选择哪一个,所以,例如,为了具体化,回忆一下我们可以为关键字制定词汇规范。

这将是像if和else这样的东西,等等,还有标识符,标识符是一个字母,后跟字母或数字,重复,好的,如果你,若查看这两项,你会发现字符串,如果if两者皆是,因此如果f是关键字语言,且也在标识符语言中。

那么应将if视为关键字或标识符,多数语言规则是,关键词总是关键词,标识符不包括关键词,嗯,明确排除关键词,写出标识符很痛苦,这是一个更自然的定义,我在这写了标识符,通过优先级排序解决,首先列出,好的。

所以有选择时,当字符串可能属于多个标记类时,首先列出的具有更高优先级,因此在我们的文件中,定义词法规范,我们会把关键字放在标识符之前。

正如我们在这里所做的,最后一个问题是做什么,若无规则匹配,若输入前缀,不在词汇规范语言中,嗯,实际上可能出现,当然有很多字符串,不在大多数语言的词汇规范中,问题是如何处理这种情况。

编译器做好错误处理非常重要,不能简单崩溃,需要能向用户反馈错误位置和类型,因此需要优雅处理,词法分析的最佳解决方案是不这样做,所以不要让这发生,好的,所以我们要做的是,相反,编写错误字符串类别。

所以所有字符串,在语言词汇规范中,我们再次写出正则表达式,这是另一个正则表达式,用于所有可能的错误字符串,所有可能发生的错误字符串,如您所知,无效词汇输入,然后将其放在最后,优先级最低。

这样它将在其他所有匹配后匹配,将其放在最后的原因,实际上允许我们在,定义错误字符串时有点粗心,实际上可以与早期正则表达式重叠,我们可以将错误字符串中实际上不是错误的内容包括在内。

但如果我们将其放在最后优先级,那么它将仅在早期正则表达式未匹配时匹配,因此实际上它将仅捕获错误字符串,然后当我们匹配,错误字符串将打印错误消息并提供漂亮的反馈,如文件中的位置等,等。

总结这个视频,嗯,正则表达式是字符串模式的很好且简洁的表示,但要在词法分析中使用它们需要一些小的扩展,特别是,我们需要解决一些歧义,我们希望匹配,嗯,是,嗯,尽可能长,因此我们一次尽可能多地输入。

所以我们会尽可能多地获取输入,我们也想选择最高,优先级匹配,因此正则表达式被赋予优先级,不同的标记类具有优先级,当出现平局时,当输入的前缀可以匹配多个时,我们选择优先级最高的。

这通常只是在文件中按顺序列出它们,列表中靠前的具有高于列表中靠后的优先级,我只想警告你,当你去写,词汇规范,当你实际实现一个语言的Alexa时,我们取最长可能匹配的规则。

以及我们倾向于最高优先级规则的平局,这导致了一些棘手的情况,并不总是明显你得到你想要的确切内容,你必须仔细考虑规则的顺序,以及你确切如何编写规则,以便你得到你想要的行为,最后,为了处理错误。

我们通常会写出捕获所有可能错误字符串的通用正则表达式,并给它最低优先级,因此它仅在,没有有效的标记类匹配时触发,输入的一部分,最后我们还没有讨论这些,但有一些非常好的算法已知用于实现所有这些。

实际上我们只能通过一次输入的遍历来做到这一点,并且每个字符的操作次数非常少,只需几次,只需简单的表查找,这将是未来视频的主题。

P13:p13 04-02-_Finite_Automata - 加加zero - BV1Mb42177J7

欢迎回到本视频,我们将讨论有限自动机,未来视频中会看到,它是正则表达式的良好实现模型。

前几视频中我们讨论了正则表达式,呃,用作词法分析的规范语言,本视频将开始新内容,我们将讨论有限自动机,作为正则表达式的实现机制非常方便,正则表达式和有限自动机密切相关,事实证明,它们可以指定相同的语言。

称为正则语言,本课程不会证明这一点,但我们会充分利用这一事实,继续前进,什么是有限自动机,嗯,这是典型的定义,您可能在自动机理论教科书中看到,有限自动机包括输入字母表,这是它可以读取的字符集,嗯,呃。

它有一个有限的状态集,应该强调这是使它成为有限自动机的原因,它有一些状态,它可以处于这些状态之一,这是特殊的,被指定为起始状态,一些状态是接受状态,所以这些是状态,嗯,我们稍后会找到更多,直观上。

如果自动机在读取一些输入后终止于这些状态之一,那么它就接受输入,否则拒绝输入,最后,自动机有一些状态转换,即,如果它在某个状态。

它可以读取一些输入并转到另一个状态,所以让我们,嗯,更详细地看一下,有限自动机中的转换,如果我在这种情况下,我写出了一个特定的转换,如果在状态一,我们读取输入a,那么自动机可以移动到状态二,好的。

自动机可以有大量不同的转换,来自不同状态和不同输入,它的读取方式是,如果在状态一输入a,我们将进入状态二,若自动机结束于接受状态,当它到达输入的末尾,它将执行称为接受字符串的操作,意味着它将说'是'。

该字符串属于此机器的语言,直观上,自动机从起始状态开始,并反复读取,输入,每次读取一个输入字符进行一次转换,因此它将查看当前状态下,基于该输入可进行的转换至另一状态,若当它完成读取输入时,呃。

处于一个最终状态,呃,那么它将接受,否则它将拒绝输入,拒绝输入的情况有哪些?若它终止,在一个状态,S,不是最终或接受状态,好的,因此若它结束于除接受状态外的任何状态,那么它将拒绝,呃,若机器卡住。

意味着它发现自己处于一个状态,且在该状态下没有输入的转换,特别是,假设它处于某个状态s,并且输入是a,并且没有转换,对于状态s在输入a上没有指定转换,因此机器无法移动并卡住,这也是一个拒绝状态。

因此在这两种情况下,若它到达输入的末尾且不在最终状态,或它从未到达输入的末尾因为它卡住,在这两种情况下,它将拒绝该字符串,该字符串不属于有限自动机的语言。

现在有一种对有限自动机的替代表示法,我认为更直观,呃,例如,我们将强调,呃,那种书写方式,嗯,在这种表示法中,一个状态表示为图中的一个节点,我们画一个大圆表示,起始状态用一个箭头指向的节点表示,无源。

这是进入节点的转换,但无源,来自的节点,这表示唯一的起始状态,接受状态画为双圆圈节点,最后,转换画为图中的节点间边,所以这表示,如果我在这个蓝色圆圈中的状态,并且我读入的输入是a。

那么我可以移动到这个箭头尾部的状态。

现在让我们做一个简单的例子,让我们尝试写出只接受单个数字1的自动机,我们需要一个起始状态,并且可能还需要一个接受状态,现在的问题是,嗯,我们中间应该放什么,这里将会有某种转换,一个好的猜测是。

如果机器读取数字1,我们应该采取该转换,现在让我花一点时间谈谈机器如何执行,所以让我们标记这些状态,让我们称这个状态为a,让我们称这个状态为b,好的,所以,嗯,机器将有一些输入,好的,我们,嗯。

我们可以在这里写出输入,所以让我们假设我们有一个单个字符1,并且它从某个状态开始,即起始状态,机器的一种配置是它所处的状态和输入,我们通常会用指针指示它在输入中的位置,说明它在输入中的位置。

关于有限自动机输入的重要事情是,嗯,输入指针总是前进,所以,当我们或它只前进,所以,当我们读取一个输入字符时,输入指针向右移动,并且永远不会后退,从状态a,我们有规则,我们可以看到我们在状态a。

下一个输入字符是1,这允许我们进行到状态b的转换,所以现在我们在状态b,我们的输入指针在哪里,它在输入的末尾,表示我们已到输入结束,所以现在这是,嗯,我们在接受状态,且已过输入结束,因此我们接受,好的。

那么让我们,嗯,再做一次执行,所以我们从状态a开始,嗯,以输入为例,嗯,字符串零,好的,我想画个指针,实际上我应该在输入前画,我们总是把指针,嗯,在这种情况下,在两个输入元素之间,紧靠左侧的一个。

我们即将阅读,在这种情况下,我们即将读零,我们处于状态a,我们的输入是零,我们查看机器,我们看到零上没有转换,好吧,因此机器卡住了,它根本没有移动,这是我们的最终配置,我们可以看到输入未结束。

因此这是一个拒绝,好的,在这种情况下,机器拒绝该字符串,因为它不属于机器的语言,嗯,再做一例,嗯,假设我们在状态well,我们始终从状态a和起始状态开始,假设这次输入的字符串是一零,好的。

输入指针在那里没问题,所以再次我们处于状态a,输入是一个一,因此我们将移动到状态b,现在输入没有变化,只是输入指针改变,但我会复制输入以显示,嗯,差异,现在输入指针已前进,因为我们已读取,嗯。

一个输入字符,现在我们处于另一个状态,现在我们可以看到,嗯,我们处于状态b,下一个输入是零,从状态b没有零的转换,尽管处于接受状态,b是终态,是其中之一,除了未消耗完输入的状态,因此。

该机器也拒绝此字符串,这也是拒绝,通常,我们可以谈论,有限自动机的语言,等于,嗯,集合,接受的优势,好的,有限自动机的语言,当谈论有限自动机的语言,我指自动机接受的字符串集。

现在让我们做更复杂的例子,尝试写出接受任意个1,后跟单个0的自动机,嗯,再次需要开始状态,嗯,还需要一个最终状态,现在让我们从思考,这台机器的语言中最短字符串开始,所以在这种情况下嗯。

我们知道它必须以单个零结尾,所以零肯定必须是零,转换必须是最后一步,在那零之前可以有任何数量的1,特别是不能有1,所以一,嗯,这台机器的转换是从起始状态,嗯,输入零,我们肯定能到达最终状态,因为单一。

由单个零组成的字符串属于这台机器的语言,现在唯一的问题是,我们如何编码任何数量的零之前都可以是一的事实,好的,有一个简单的方法,嗯,我们可以在起始状态添加一个自循环,嗯,并采取那个转换。

如果我们读到一个一,这意味着什么,这意味着只要我们在读一,我们就会停留在起始状态,然后一旦我们读到一个零,我们将移动到最终状态,因为那必须是,嗯,字符串的结束,嗯哼,如果这,如果机器要接受它。

所以让我们做几个例子来说服自己这可行,让我再次标记这些状态,所以这是状态a,那个状态b,所以嗯,让我们在这里写出来,状态和输入,我们将从状态a开始,并输入,嗯,一一零,好的,所以让我们先做一个接受案例。

好的,输入指针开始于第一个字符的左侧,我们在状态a,在起始状态我们读到一个一,这说我们应该采取一个转换,让我们回到状态a,然后我们前进输入指针,好的,现在我们消耗了第一个一,并且我们再次在状态a。

下一个输入是一个一,所以我们将进行另一个转换,嗯到状态a,输入指针将前进,好的,所以现在我们在状态a,下一个输入是一个零,所以我们将采取转换到b,现在我们在这种配置中,所以输入指针已经达到了输入的末尾。

嗯,我们在接受状态,所以机器接受一一零,一是这个机器的语言,所以现在让我们做一个例子,我们将拒绝输入,我们开始于哪个配置?有限自动机的配置,意味着你知道执行中的一个点。

它总是由一个状态和输入指针的位置组成,所以我们的初始状态是a,现在让我们选择字符串,哦,我不知道,让我们取100,让我们确认这不在机器的语言中,好吧,我们从状态a开始,输入指针在那里,现在我们读一个1。

这意味着从状态a,转换1,我们留在状态a,输入指针前进,现在我们看到一个0,所以从状态a输入0,我们转换到状态b,现在输入指针在这里,所以现在,嗯,我们在状态b,输入是0,但状态b上没有0的转换。

状态b上没有任何转换,所以机器卡住了,它不能读完输入,再次,即使我们在接受状态,我们还没有读完整个输入,所以这意味着机器会拒绝,所以100,0不在这个机器的语言中。

到目前为止,有限自动机每次移动都会消耗一个输入字符,所以只要它能做任何移动,输入指针就会前进,嗯,现在我们要谈论一种新的移动,epsilon移动,epsilon移动背后的想法是机器可以做出状态转换。

而不消耗输入,所以例如,如果我有一个状态,我在状态a,我的输入,让我们就说,嗯,我们有x1,x2,x3,出于某种原因,当我们准备读取x2时我们做了一个epsilon移动,机器改变状态。

但输入指针仍然在完全相同的位置,所以机器的新配置将是我们在状态b,但我们的输入指针仍在等待读取x2,可以将epsilon移动视为机器的一种免费移动,它可以,它可以不消耗任何输入移动到另一个状态,嗯。

为了清楚起见,机器不必进行epsilon移动,这是一个选择,我们可以决定是否进行epsilon移动。

现在,Epsilon移动是第一次,我们提到有限自动机可能有一个选择,在它的移动上,实际上,在具有选择性的自动机和没有选择性的自动机之间有一个重要的区别,因此确定性的,有限自动机有两个属性,首先。

它们没有epsilon移动,因此它们必须始终消耗输入,其次,对于每个输入和每个状态,它们只有一个转换,我指的是什么,这意味着,如果我看一个确定性的自动机中的任何状态,它们永远不会有像这样的事情。

对于相同的输入,它们有两个可能的移动,确定性的自动机中的所有出站边都必须有不同的输入标签。

然后非确定性的,有限自动机就是那些不是确定性的,特别是nitrachaton可以有epsilon移动,因此它可以选择不消耗输入而移动到另一个状态,它们也可以在一个给定的状态下为同一个输入有多个转换。

所以像这样的事情是可以的,对于一个非确定性的自动机,现在让我指出,实际上,Epsilon移动足以创建非确定性的自动机,而这个第二个情况,你在同一个输入上有多个转换。

可以通过一个稍微更复杂的带有epsilon移动的机器来模拟,因此,例如,我可以以以下方式绘制这个机器,我可以有,或者我可以以以下方式模拟被圈出的机器,我可以有一个带有两个epsilon移动的状态,然后。

每个这些状态都有一个在a上的移动,所以,如果我标记这些状态为1,2和3,那么这个将对应于状态一,那么这个将对应于状态二,那么这个将对应于状态三,因此,任何时候我们有一个状态在单个输入上有多个移动。

我们总是可以用更多的带有epsilon移动的状态来替换它,并让机器中的每个状态对于每个可能的输入只有一个转换,所以实际上,确定性的自动机和非确定性的自动机之间的唯一根本区别,是否有epsilon转移。

确定性自动机的关键属性是,对于每个输入,它只能通过状态图的一条路径,因此这是每个输入,我所说的意思是什么?即自动机始终从起始状态开始,让我们考虑一个非常简单的输入字符串abc。

如果我们看确定性自动机将采取的状态序列,嗯,对于那个输入,通过状态图的这个路径完全由输入决定,因为再次,在给定状态下,它没有选择,只会有一个标记为a的转换,它将带您到一个只有标记为b的转换的状态。

那将带您到另一个只有标记为c的转换的状态,因此,每个输入决定了自动机将通过状态图采取的路径,这对非确定性自动机不成立,因此,例如,可能是,从起始状态开始,输入a,对于输入a,我可以到达某个状态i。

但可能还有另一个转换,嗯,标记为a,那将带我到另一个状态,因此,自动机可能能够到达两个不同的状态,并且可能还有epsilon转换,因此,对于非确定性自动机来说,通常,当它们通过状态图时。

当它们在输入上执行时,它们可能会最终处于任何数量的不同的状态,好的,关于非确定性自动机的规则,嗯,关于何时接受是,如果任何路径接受,那么nfa,接受,如果一些,导致接受状态在输入结束时。

这就是非确定性自动机,我可以选择做什么移动,只要有一些选择它可以做,那将使它到达接受状态,所以让我们说,嗯,切换颜色这里,你知道,这是一个接受状态在这里,它走了这条路,那么它将接受。

也许所有这些其他路径都是拒绝的,路径,那不重要,只要有一条路径,嗯,一组nfa可以选择的动作,让它在输入结束时到达接受状态,我们说该字符串属于NFA语言。

NFA能进入多个不同状态的事实,取决于它们在运行中的选择很重要,将在未来视频中发挥核心作用,我们将快速举一个例子,以确保这一点清晰,这是一个小自动机,注意它是非确定的,从起始状态,输入零有2种可能移动。

我们将执行该机器的一个执行,在一个样本输入上,看看它能进入哪些不同状态,我们从起始状态开始,我们应该标记我们的状态实际上,以便我们可以引用它们,让我们称它们为a、b和c,并且假设第一个输入是1。

那意味着什么,这意味着我们将采取这个转换,我们将从起始状态回到起始状态,机器可能处于的状态集,在第一次转换后只是集合a,所以保证仍然在起始状态,所以没有,与。的选择,呃,第一个移动现在。

假设第二个输入字符是零,现在我们有选择,我们可以去状态b或我们可以去状态a,我们可以认为这然后是一个可能性集,在我们执行这个移动后,这个转换,机器可能处于任何一个集合状态。

实际上这完全描述了机器的可能性,我们知道我们已经读了第二个输入字符,现在我们可以处于一个集合状态,好的,我们可以处于状态a或状态b,所以现在,假设我们读另一个零,我们可能去哪里,那么嗯。

如果我们在状态b,我们可以做转换到状态c,但如果我们处于状态a,那么我们将做转换,要么到状态b,再次到状态a,所以事实上,如果我们读另一个零,我们可以处于任何一个三个状态,好的,现在你可以看到,嗯。

规则是什么,所以,每一步,非确定性自动机处于机器的状态集合中,当读取另一个输入时,考虑所有可能的移动,它可以计算,机器在下一步可能处于的状态完整集合,那么最后我们如何决定机器是否接受。

我们查看最后输入位读取后的最终状态,如果有任何,抱歉,我们查看最后输入字符读取后的最后状态集合,如果该集合中有任何最终状态,那么机器接受,在这种情况下,我们读取零后。

我们看到接受状态c在这个可能状态集合中,这意味着机器可以做出一些选择,在输入结束时进入最终状态,因此,机器接受这个输入,好的,所以,如果最终可能状态集合中有最终状态,那么非确定性机器接受。

结果表明,NAS和DFA识别完全相同的语言,特别是正则语言,因此NAS,DFA和正则表达式具有相同的功能,它们只能指定正则语言,DFA执行速度肯定更快,嗯,主要或完全,因为不需要考虑选择。

所以DFA可以沿着状态图的单一路径前进,而与NFA,我们可能需要跟踪NFA中的潜在选择集,我们可能处于一些状态中,然而,NAS有一些优点,它们通常比DFA小得多,实际上,它们可以比DFA小指数倍。

所以最小的uh,NFA,Uh可能比最小的等效DFA小得多,Uh,啊,比相同语言的等效最小DFA小,因此本质上,NAS和DFA之间存在时空权衡,NAS可能更紧凑。

P14:p14 04-03-_Regular_Expressi - 加加zero - BV1Mb42177J7

欢迎回到本视频,将正则表达式转换为非确定性有限自动机。

开始前,先概述计划,接下来的几段视频,有一个词法规范要实现,第一步是有人将其写为正则表达式集,当然这本身不是实现,只是规范,必须将其翻译为可进行词法分析的程序,实际上分几步,第一步是。

将正则表达式转换为识别相同的非确定性有限自动机,完全相同,然后将这些非确定性自动机转换为确定性自动机,最后将这些确定性自动机实现为查找表,和一些遍历这些表的代码,之前视频中已讨论过,并已定义这些部分。

现在准备整合,本视频将聚焦于此组件,这里,将正则表达式转换为非确定性有限自动机。

计划是对于每种正则表达式,嗯,定义一个等效的,非确定性自动机,该自动机接受与正则表达式语言完全相同的语言,将使用以下符号,为正则表达式定义自动机,通常需要修改,它们的起始状态和最终状态。

箭头表示起始状态,双圆圈表示最终状态,无需太关注机器的整体结构,只要把握起始状态和最终状态,应该说在构建的机器中,嗯,将只有一个最终状态,好的,那么对于空正则表达式,机器是什么,嗯,接受它的呢?

这是一个非常简单的机器,只有起始状态、最终状态和它们之间的空转换,该机器仅接受空字符串,类似地,对于单个字符a,我们可以定义一个单转换状态机来接受该字符,接受该字符的一转换状态机,从起始状态可到终态。

仅当我们读特定字符,好的,所以这是我们的两个,呃,简单,呃正则表达式,现在我们必须,呃,做复合正则表达式,这些稍微复杂些,所以先谈,呃,连接,因为我们将从小正则表达式构建大机器。

可假设已将a和b分别转换成机器,我有a的机器,我有b的机器,现在只需说如何粘贴,这两个机器形成机器,复合机器识别与ab连接相同的语言,这是构造,复合机器的起始状态是a的起始状态,保持a的起始状态不变。

然后修改a的终态,我们使a的终态,不再是终态,这里通过移除a终态的双圆圈完成,并向b的起始状态添加epsilon过渡,那确实做对了,这意味着首先识别输入的一部分属于a的语言,当我们到达,本应是a的终态。

我们可以跳转到b的起始状态而不消耗输入,然后尝试读取剩余的字符串,呃,作为语言的一部分,作为b语言中的字符串,对于并,呃,我们有类似的方法粘贴机器,尽管结构有些不同。

所以这里为复合机器添加一个新起始状态,a加b意味着输入属于a的语言,或属于b的语言,epsilon过渡非常适合捕获这个,因为我们直接从起始状态做出决定,这个字符串属于a的语言,还是属于b的语言。

所以我们做出非确定性选择,然后使用我们选择的自动机读取字符串,如果我们到达最终状态,这两个机器中的任何一个,我们可以进行epsilon转换到复合机的最终状态,现在记得非确定性自动机的接受概念。

你知道他们做出这些猜测,但如果有任何猜测有效,那么我们说它在机器的语言中,所以如果,实际上,字符串是a或b的并集,那么选择a或选择b都会有效,因此机器将接受该字符串,最后是最复杂的情况,嗯。

对于迭代a星,我们有以下构造,这是a的机器,嵌入在这里,我们添加了一个新的开始状态和一个新的最终状态,现在让我们谈谈它是如何工作的,所以有一种可能性是记住epsilon始终是a星语言的一部分。

所以我们有这个转换,我们可以直接从开始状态到最终状态并接受空字符串,所以嗯,这仅仅确保空字符串在语言中,否则我们做什么,否则我们可以进行epsilon转换,到a的开始状态,然后我们可以从a的最终状态。

如果我们到达它,我们可以回到整个机器的开始状态,我们可以这样做任意多次,好的,所以a的迭代围绕这个循环在这里和右边,当我们到达a的最终状态时,我们也可以决定直接进行转换到机器的最终状态。

我们得出结论那是最后一次,因此这台机器识别a语言中的零个或多个字符串,所以现在让我们做一个例子,这是一个正则表达式,我们想要构建一个等效的非确定性机器,以识别相同的语言,我们将遵循我们的构造,嗯。

它通过归纳法在正则表达式的结构上工作,从简单的正则表达式开始,构建到复合的,所以这里我们有什么,所以我们有一个接受1的机器,好的,所以我们需要一个机器,如果它被称为had,接受两个状态,嗯,就这样。

你知道,在数字1上完成了过渡,嗯,同样,一个接受零的机器,好的,现在我们需要将它们组合成一个接受1或0的机器,我们这样做的方法是,嗯,从。中选择,从复合机的起始状态,我们可以移动到接受1的机器。

或接受0的机器,然后在结尾,还有回到复合机终态的epsilon移动,好的,现在我们需要迭代这个,因此需要能够接受0个或多个1或0,我们将整个块粘贴到,我们为迭代设置的图案中,我们如何做到呢,嗯。

我们有新起始和新终点,好的,从起始到新终点有epsilon移动,确保接受空串,然后可迭代内机任意多次,可做epsilon移动到起始,可执行,机器一次,若想再做,可再次进行,好的,能做到,好的。

再循环一次,若已看够,可从最终状态决定,直接进入复合机的最终状态,此机接受语言1+0*,还有更多工作要做,必须接受,嗯,我们需要另一台只接受一个的机器,所以我们又造了一台接受,数字1的机器。

现在我们需要一个来组合这两个东西以连接它们,这非常简单,我们只需要一个epsilon移动,从第一个机器的最终状态到第二个机器的起始状态,然后这些都是最终机器的状态,嗯,我们实际使用的最终状态。

最终是整个机器的最终状态,即这个状态和起始状态,这是非确定,自动机的整个构造,或非确定自动机,识别该语言。

P15:p15 04-04-_NFA_to_DFA - 加加zero - BV1Mb42177J7

欢迎回到本视频,我们将讨论将非确定,有限自动机转换为确定有限自动机。

这里再次是我们的词法分析器,构建流程图,因此,从词法规范开始,我们编写正则表达式,上次我们讨论了这一步,将正则表达式转换为非确定有限自动机,这次我们将讨论这一步,你可能猜到了,在本系列视频的最后一集中。

我们将讨论最后一步,即DFA的实现。

这是上次我们完成的非确定有限自动机,今天我们要讨论的第一个重要概念,称为状态ε闭包,基本思想是,我选择一个状态,它可以是一组状态,但我们只对一个状态进行操作,然后我看我能通过。

仅跟随ε移动到达的所有状态,所以b是我们要开始的状态,所以b将包含在集合中,然后有一个到c的ε移动,所以c将包含在集合中,还有一个到d的ε移动,所以d将包含在集合中,因此,我们称b的ε闭包等于集合。

b c d,让我们再做一例,嗯,作为示例,让我们取g的ε闭包,当我们换颜色时,我将做这个,我将擦掉这个,用粉红色或紫色做这一个,所以g的ε闭包,嗯,我们必须跟随g的所有ε转换。

所以h将包含在g的ε闭包中,但不仅仅是一个ε移动,这是递归的,我可以采取的任何数量的ε移动,所有这些状态都包含在g的ε闭包中,实际上我也会被包括在内,A将被包含在内,b和c和d也将被包含在内,现在。

如果我查看所有这些用淡紫色着色的状态,我可以看到,仅使用ε移动,我无法从这些状态到达任何新状态,因此,g的ε闭包将等于,现在让我们在这里全部写出来,是 a b c d g h i 好吧。

所以这是状态epsilon闭包。

回忆一下上个视频,nfa在任何给定时间点可能处于多个状态,这是因为对于给定的输入,它可以做出的选择,nfa可能到达多个不同的状态,现在我们要解决的问题是有多少不同的状态它可以处于。

如果一个非确定性自动机有n个状态,并且最终处于这些状态的一些子集中,那么子集可以有多大呢,显然,该集合的基数必须小于或等于n,因此,nfa最多可以进入n个不同的状态,如果我想知道不同子集的数量,嗯。

n个东西有多少不同的子集呢,这意味着有2的n减1次方个可能的n状态子集,这个数字非常有趣,首先,它是一个非常大的数字,因此,nfa可以进入许多不同的配置,特别是那些具有许多不同状态的一个,但重要的是。

这是一个有限的可能配置集,并且,这将成为将nfa转换为dfa或确定性自动机的想法的种子,因为所有我们需要做的就是能够转换一个非,确定性自动机到一个确定性自动机,是让确定性自动机。

模拟非确定性自动机的行为,并且非确定性自动机只能进入有限数量的配置,即使该配置集非常大,这正是我们将利用的,嗯,在构造中。

现在我们已经准备好给出构造,展示如何将任意非确定性有限自动机,映射到等效的确定性有限自动机,所以让我们从说明nfa中有什么开始,所以我们将有一个状态集,我们称之为s,这些是非确定性机器的状态。

有一个起始状态,嗯,小s,是状态之一,还有一个终结状态集,大写F,然后我们还必须给出转换函数,我想要写出状态转换函数,我想使用状态转换函数来定义一个操作符,我们将发现对于定义我们的dfa非常方便。

所以让我们说a应用于一个状态集,所以x这里是一个状态集,a是一个输入语言中的字符,所以a(x)等于那些状态y,使得存在一些x,使得a(x)=y,其中小x是状态集中的一个状态,a是输入语言中的一个字符。

所以a(x)等于那些状态y,使得存在一些x,使得a(x)=y,集合中的一个状态,使得从x到y有输入a的转换,这仅是表示在集合级别给出转换函数的一种方式,对于给定的状态集。

X显示所有可以通过输入a到达的状态,然后我们需要的是我们定义的epsilon闭包操作,嗯,几页幻灯片之前,我将简写为epsilon-cs,epsilon闭包,现在我们可以定义DFA,DFA会是什么样呢?

它必须包含所有这些,它必须有一个集合,它必须说明状态是什么,起始状态是什么,终结状态是什么,以及转换函数是什么,让我们从状态集开始,嗯,各州将是,子集,因此,DFA的状态将是,NFA状态的子集,因此。

对于每个可能的,NFA状态的子集,当然,这可能是很大的数字,但仍然是有限的,因此,我们可以使用这些,状态子集作为确定性机器的状态空间,现在DFA的起始状态是?嗯。

将是非确定机起始状态的epsilon闭包,确定性机器,如果你考虑一下,这很有意义,我们想做的是,我们想跟踪非确定性机器可能处于的状态集,DFA的每个状态对应非确定性机器的不同子集,因此。

DFA的每个单独状态,它告诉我们NFA可能处于的特定状态集,我们可能处于哪些州?显然,NFA从其起始状态开始,但在读取任何输入之前,它可以进行epsilon移动,因此在读取,第一个输入符号之前。

它可能处于的状态正是起始状态的epsilon闭包,现在,终态集合是什么?嗯,终态集合将包括那些状态x,记住,记住,DFA的状态是NFA状态的集合,所以x是一个集合,它将包含所有x。

使得x与NFA的最终状态集相交不为空,因此,DFA的任何状态,只要包含NFA的一个状态,一个最终状态,抱歉,NFA中的,它足以作为DFA的最终状态,因为记得NFA的目标只是它有一些计算,接受输入。

这意味着有一些方法可以到达最终状态,因此,如果任何状态,它是最终状态,我们很高兴,我们可以在这里捕获它,通过仅考虑包含NFA至少一个最终状态的所有状态,作为DFA的最终状态,最后,我们需要定义转换函数。

我们如何做到这一点?我们需要说,对于给定的状态x和另一个状态y,当它们之间存在转换时,在某输入a上,那么这种转换在什么条件下存在?让我们写出来,所以记得我们处于状态x,我们需要知道什么?

我们需要知道在输入a上可以到达的状态集,我们刚刚定义了,那是a(x),然后,一旦我们到达这些状态之一,一旦我们看到从状态集x在输入a上可以到达的位置,还有可能在之后进行epsilon移动,因此。

我们还需要取,呃,这些状态的epsilon闭包,好吧,嗯,我们将说存在一个转换,呃,从x到y,如果y等于这些状态集,并且请注意,对于任何x,只有这样的状态集一个,这确保了这是一个确定性机器。

每个状态在每个输入上只有一次可能的移动,因此,我们现在可以检查清单,看看我们是否有确定性机器,我们有一个有限的状态集,我们有一个起始状态,我们有一个最终状态集,我们有一个转换函数,每个输入只有一个移动。

没有epsilon移动,因此,这实际上是一个确定性机器,它保持的性质是,计算的每一步,DFA的状态记录,NFA可能在该输入上达到的状态集。

所以让我们通过一个例子来构建一个确定性机器,从一个非,确定性机器,这是上次视频中构建的NFA,再说一次,这是视频开头定义epsilon闭包时使用的,我们将做示例,与上一页的构造略有不同。

如果实际写出所有这些状态的子集,将花费我们非常非常长的时间,事实证明,并非所有子集都被DFA使用,我们只需枚举实际需要的状态,我们将从DFA的起始状态开始,然后找出需要哪些附加状态,我们如何做到这一点。

我们从NFA的起始状态开始,即这个状态a,然后回忆DFA的起始状态是该状态的epsilon闭包,因此对应于这个紫色集合,因此DFA的第一个状态,起始状态是状态a、b、c、d、h、i的子集。

现在我们必须找出,嗯,从这个特定状态,从起始状态,对于每个可能的输入值会发生什么,这台机器的字母表是1和0,因此从这个状态必须有两个转换,一个输入为1的转换,一个输入为0的转换,我们先做输入0。

我们可以看到,紫色集合中只有一个可能的转换,那就是从状态d到状态f,因此状态f肯定包含在NFA可到达的状态集中,但一旦我们到达状态f,我们可以进行很多epsilon移动。

因此事实上DFA的第二个状态对应于一个更大的集合,它是所有,它是状态f的epsilon闭包,即这个状态集f、g、h、i、a、b、c和d,好的,这是NFA在读取单个零后可能处于的状态集。

接下来让我们考虑从起始状态在输入1上会发生什么,NFA可能到达哪些状态,如果查看转换函数,我们可以看到NFA可能有两种可能的移动,它可能在状态c,在这种情况下它将移动到状态e,或者它可能在状态i。

这也是紫色集合的一部分,在这种情况下它将移动到状态j,因此,由于读取1,NFA可能进入的两个可能状态,然后在那之后可以进行一系列的epsilon移动,实际上,阅读一个一后,机器可能处于任何状态。

除了状态f,这是这些状态集合,你会发现这个特定状态集合,红色集合包括NFA的最终状态,所以这也是一个最终状态,表明阅读一个一后,NFA可能处于接受状态,因此这将是DFA的接受状态,嗯。

我们仍然需要填写我们添加的两种状态,机器右侧的两个状态,它们在输入零时做什么,它们在输入一时做什么,让我们弄清楚,所以从红色状态开始输入零,会发生什么?看,红色状态包括状态d,它可以移动到状态f。

但我们已经计算过,epsilon会发生什么,epsilon闭包f是什么,那就是绿色状态,所以如果我处于红色状态并读取一个零,我移动到绿色状态,如果我处于红色状态并读取一个一,你会看到两个状态。

NFA状态c和i在红色集合中,所以它只是把我们带回红色集合,类似地对于绿色状态,如果我读取一个一,我移动到红色状态,如果我读取一个零,我留在绿色状态,所以这然后是我们下面的确定性机器,这是确定性机器。

它模拟NFA,所以确定性机器的每一步,它记录了NFA可能处于的状态集合,它将接受一个字符串。

P16:p16 04-05-_Implementing_Fin - 加加zero - BV1Mb42177J7

欢迎回到本视频。

我们将结束关于词法分析的演讲,通过使用各种不同技术实现有限自动机进行讨论。

简单回顾,这是词法分析器构建的小流程图,今天我们将重点关注这最后一步,DFA的实现,实际上我应该说这张图表并不完全准确,因为有时我们不会走到DFA,我们停在NFA并直接实现它们,我们也会稍微谈谈这一点。

如果我们不想构建DFA,而是想直接基于NFA构建词法分析器。

现在从DFA开始,嗯,实现确定性有限自动机非常简单,只需使用数组,这个二维数组,其中一个维度是状态,所以我可能会有这些状态,另一个维度是输入符号,所以我可能会有状态i和输入a,我简单地查找那个位置。

那里将是我们将要移动到的下一个状态k,因此,表格在特定输入符号和状态下存储,机器将移动到的下一个状态。

所以,让我们举一个将确定性自动机转换为表格的例子,驱动实现,这是我们上次构建的自动机,回忆一下几段视频前我们从正则表达式开始,它被转换为一个非确定性有限自动机,然后我们使它,我们将其转换为确定性自动机。

就在上一个视频中,它又在这里,现在让我们谈谈如何将其实现为表格,所以我们画一个二维表格,有三个状态,所以我们需要三行,我将简单地标记这些行s t和u,然后有两个输入,零和一,现在让我们填写表格中的条目。

所以在状态s输入零,我们去哪里,我们转到状态t,所以s零,条目将是t,类似地,嗯,从状态s输入,一将最终到达状态u,因此,第一个条目将是您好,然后类似地,对于其他i,表格的其他行,让我们接下来做t行。

一我们转到u,在零我们留在t,所以这行也是tu,最后对于u会发生什么,嗯,在零我们转到t,在一我们留在u,所以这行也是tu,这就是描述自动机转换关系的表格,现在如果我们考虑如何在程序中使用这个转换关系。

你可以很容易地想象我们会开始,比如说,我们的输入索引,嗯,指向输入的开头,让我们称之为零,我们还需要有一个当前状态,我们从起始状态开始,让我们简单地说,那是第零行,在这种情况下,那将是行s。

然后当我们想要做的,我们想走过输入并检查是否i,嗯,并检查那个,你知道,并做出转换,嗯,对于输入的每个元素一次,我们想要停止时输入为空,所以只要还有输入,假设我们有一个字符数组,那是我们的。

那是我们的输入,只要数组中的条目不为空,嗯,让我们做以下操作,我们将更新状态,嗯,在每个步骤,我们要更新它到什么,嗯,让我们给这个数组起个名字,让我们称之为数组a,所以我们要在我们的转换关系中查找a。

我们要查找什么?我们要查找当前状态,我们要查找输入,在那个条目中,你知道,使用当前的状态和当前的输入,我们将转换到一个新状态,我们还想增加输入指针,所以我们会同时做这件事,这就是处理输入的循环。

根据转换表a,正如您可以看到的,这是一个非常紧凑的,非常高效的过程,只是一点点索引运算,和一两个表查找,一个输入和一个状态,每个输入字符的转换表,很难想象,嗯,有一个比这更快的过程,或更紧凑。

现在是一种实现确定有限自动机的策略,和,你可能已经注意到那个特定方法的缺点是,表中有很多重复的行,实际上,表中的所有行都完全一样,我们可以通过稍微不同的表示来节省一些空间,所以,我们不需要一个二维表。

我们可以只用一个一维表,这个表同样,嗯,是一个,每个状态的条目,所以 s t 和 u,这个表将包含一个指向特定状态移动向量的指针,所以这里有一个指针,它将指向另一个一维表。

该表将说明我们在零和一上应该做什么,所以在 s 的案例中,我们想移动到状态 t,如果是零,和状态 u,如果是一,现在当我们去 ah,填写 t 的条目时,我们看到不需要复制这行,实际上可以共享这行。

类似地对于 u,所以这个表示实际上更加紧凑,我们共享自动机中重复的行,在我们为词法分析考虑的自动机中,非常常见的是有重复的行,这实际上可以导致表的显著压缩,特别是当你考虑到可能的状态数时,记住。

对于一个具有 n 个状态的 NFA,DFA 可能有 2 的 n 减一次方个状态,嗯,具有 n 个状态,虽然膨胀通常不是最坏情况,它可以非常显著,我们之前幻灯片上的二维表实际上可能非常大。

有时可通过小技巧,如,这种特定表示的缺点是,这些指针实际上需要时间,因此,我们的内循环将稍慢,我们不仅要进行表查找,解引用,指针进行另一个表查找。

然后我们可以移动,最后,也可能根本不想,将特定规范转换为dfa,因为表格可能变得真正巨大,我们可能最好直接使用nfa,因此,可以想象nfa的实现,嗯,我们还可以实现,嗯,通过表格,嗯,在这种情况下,嗯。

我们需要为nfa中的,每个状态有一行,在nfa中,我们不会全部完成,但可以为nfa的每个状态,有行,你知道我们将要去哪里,如果输入是零,或如果输入是一,所以在这种情况下,我几乎忘了,我们还需要一个转换。

在最简单或最直接的,fa实现中,我们将去哪里,如果在epsilon上,现在我记得,因为这些是nas一般,这将是状态集,因为我们可能有不止一个epsilon转换,或不止一个零和一上的转换,在这种情况下。

epsilon a可以转到b,所以这将是一组状态,B和b可以转到c或d和c,嗯,只能转到e在一上,嗯,在一上d可以转到f,嗯,输入为零,等等,我们填写其余的表格,此表格保证相对较小。

因为状态数受nfa中状态数的限制,和输入字母表的大小,再一次,我们可以共享行等技巧压缩表,如果我们喜欢,但现在模拟自动机的内循环将更昂贵,因为我们要处理状态集而非单个状态,所以每个时间点。

我们将跟踪一组状态,当我们进行移动时,我们得为集合中的每个状态查找潜在去向,包括epsilon移动和执行所有可能的epsilon移动,因此我们总能准确评估NFA可能到达的状态集,虽然这节省了表空间。

就表的大小而言,它可能比确定性自动机执行得更慢。

总结词法规范实现的关键思想,是将非确定性有限自动机转换为确定性有限自动机,这是将一般,呃,高层规范使用正则表达式,转换为完全确定性的东西,现在实践中,工具提供速度和空间之间的权衡,所以DFA更快,呃。

不那么紧凑,所以表可能非常大,有时这是实际问题,NFA实现起来更慢,呃,但更简洁,工具给你,呃,通常,一系列选项,并且通常以配置文件或命令行形式,开关允许你选择,是否想要更接近完整的DFA,更快。

也许更大,或纯NFA,更慢。

P17:p17 05-01-Introduction_to - 加加zero - BV1Mb42177J7

本视频将过渡到词法分析到解析,并简单讨论这两个编译阶段的关系,我们已经讨论了正规语言。

值得注意的是这些是最弱的广泛使用的形式语言,但它们有,当然,许多应用,其中一些我们在之前的视频中看到了。

现在正规语言的困难在于许多语言根本不符合规则,并且有一些非常重要的语言无法使用正则表达式表达,或有限自动机,所以让我们考虑这个语言,它是所有平衡括号的集合,所以该语言的一些元素将是字符串。

每个n一个开一个闭,两个开括号,两个闭括号,三个开括号,三个闭括号等等,你可以想象这实际上代表了,许多编程语言结构,例如,嗯,任何类型的嵌套算术表达式都将属于此类,但还有像嵌套的。

如果那么否则也将具有这个特征,在这里嵌套的,如果那么否则,只是if语句像开括号一样起作用,不是所有的语言都像cool,它有明确的关闭费用,但在许多语言中它们是隐式的,因此编程语言中有许多嵌套结构,构造。

并且那些不能由正则表达式处理,所以这提出了一个问题,正规语言可以表达什么,以及为什么它们不足以识别任意嵌套结构,因此我们可以通过查看一个简单的两状态机,来阐明正规语言和有限自动机的局限性。

所以让我们考虑这个机器现在我们有一个开始状态,然后另一个状态是接受状态,我们将让这个机器只是一个我们已经见过的机器,它将识别包含奇数个1的字符串,所以如果我们看到一个1并且我们在开始状态,我们移动。

我们现在看到了奇数个1,我们移动到接受状态,并且我们留在那里直到我们看到另一个1,在这种情况下我们看到偶数个1,然后我们处于开始状态,这就是这个机器,每当看到奇数个1,我们在最终状态,每当看到偶数个1。

我们在起始状态,如果给定一个较长的1串,让我们选择包含7个1的,它会做什么,它将在这两个状态间来回,当到达最后一个1时,它将处于最终状态,所以它将接受,但注意,它不知道访问最终状态多少次。

它不记得字符串的长度,它无法计算字符串中有多少字符,实际上,我只能计算奇偶性,总的来说,我有有限时间,所以我只能表达,你可以计数的东西,模k,他们可以计数,嗯,模k,嗯,对于某个k。

其中k是机器的状态数,所以你知道,如果有三个状态,机器,我可以跟踪字符串长度是否可被3整除或其他类似属性,但我不能做像计数到任意高这样的事情,因此,如果需要识别需要任意计数高的语言。

比如识别所有平衡括号的字符串,你不能用有限的状态集做到。

那么解析器做什么,它从词法分析器接收标记序列作为输入,并产生程序的解析树。

例如,在Cool中,这是一个输入表达式,作为词法分析器的输入,词法分析器产生这个标记序列作为输出,这是解析器的输入,然后解析器产生这个解析树,其中嵌套结构已明确,我们有if,然后else。

然后是三个组件,谓词,然后分支,和else分支的if,总结。

词法分析器以字符串字符作为输入,产生标记字符串作为输出,该标记字符串是解析器的输入,解析器产生解析树作为输出,将字符串标记解析为程序解析树,这里值得提几点,首先,有时解析树是隐式的。

编译器可能永远不会构建完整的解析树,我们稍后会详细讨论,稍后,许多编译器确实构建了显式的解析树,但许多没有,另一件值得提的事,是有些编译器将这两个阶段合并为一个,所有工作都由解析器完成。

因此解析技术足够强大,可以表达词法分析和解析,但大多数编译器仍按这种方式划分工作,因为正则表达式与词法分析非常匹配。

P18:p18 05-02-_Context_Free_Gra - 加加zero - BV1Mb42177J7

本视频中,我们将从上下文无关文法开始讨论解析技术。

现在,如我们所知,并非所有标记序列都是有效的程序,解析器必须区分它们,它必须知道哪些标记序列是有效的,哪些是无效的。

并为无效的序列提供错误消息,因此,我们需要一种描述有效标记序列的方法,然后,某种算法来区分有效和无效的标记序列。

我们还讨论了编程语言具有自然的递归结构,例如,在Cool中,呃,一个表达式可以是许多不同事物之一,所以其中两个可以是if表达式和while表达式,但这些表达式本身是由其他表达式递归组成的,例如。

if的谓词本身是一个表达式,然后分支和else分支也是,在while循环中,终止测试是一个表达式,循环体也是如此,上下文无关文法是描述这种递归结构的自然表示。

那么上下文无关文法是什么,所以正式来说,它由一组终结符,T,一组非终结符,N,一个开始符号,S,且S是非终结符之一,以及一组产生式组成,那么什么是产生式,一个产生式是一个符号,后跟一个箭头。

后跟一个符号列表,这些符号,呃,有一些规则,所以箭头左侧的x必须是非终结符,这就是在左侧的意思,所以产生式左侧的所有事物都是非终结符,然后右侧,每个yi右侧可以是非终结符,或它可以是终结符。

或它可以是特殊符号,Epsilon。

让我们做一个上下文无关文法的简单例子,平衡括号的字符串,我们在之前的视频中讨论过,可以表示如下,我们有开始符号,一个平衡括号字符串的可能情况是它包含一个开,另一组平衡括号和闭括号。

另一组平衡括号的可能为空,因为空字符串也是平衡括号,所以语法有两个产生式,回顾一下,将此示例与前一幻灯片给出的正式定义相关联,我们的非终结符集是什么,仅有一个非终结符s,在这个上下文中。

我们的终结符号是什么,仅是开括号和闭括号,或其他符号,开始符号是什么,它是s,它是唯一的非终结符,所以它必须是开始符号,但通常我们会给出语法,第一个产生式将命名开始简单,所以而不是明确命名。

哪个产生式首先出现,左侧符号将是该上下文自由语法的非终结符,最后,生产式是什么,那里,我们有点,它可以是一组产生式,这里是两个产生式,呃,对于这个特定的上下文,自由语法,现在,生产式可以读作规则。

所以让我们写下示例语法中的一个产生式,这意味着什么,这意味着,无论我们看到一个s,我们可以用右侧的符号串替换它,所以无论我看到一个s,我可以替换,我可以取出s,这是重要的,我从左侧移除s。

并用右侧的符号串替换它,所以,生产式可以读作替换规则,右侧替换左侧,所以这里有一个更正式的过程描述,我们从一个只有开始符号的字符串开始,所以,我们总是从开始符号开始,现在,我们看看我们的字符串。

最初它只是一个开始符号,但它随时间而改变,我们可以用某个非终结符的产生式的右侧,替换字符串中的任何非终结符,例如,我可以用x的某个产生式的右侧替换非终结符x,在这种情况下,x变为y1到y n。

然后重复步骤二,直到无非终结符,直到字符串仅含终结符。

此时完成,稍正式写为,一步包括一个状态,即符号串,含终结符和非终结符,此串中某处有非终结符x,语法中有x的产生式,这是语法的一部分,好的,这是一个产生式,我们可以使用这个产生式,嗯,从,到新状态。

其中我们已替换,X为产生式右端,好的,这是上下文无关推导的一步,现在若要执行多步,我们可以有一堆步骤,Alpha零,进入Alpha一,进入Alpha二,这些都是字符串,现在,Alpha i都是字符串。

随着我们前进,最终得到字符串Alpha n 好吧,然后说alpha零重写为alpha n,零步或多步,这意味着零或更多步,对吧,这只是一个简写,表示,存在一系列个别产生式,个别规则应用于字符串。

使我们从alpha零到alpha,记住,我们通常从开始符号开始,因此,我们有一系列步骤像这样,从开始符号到其他字符串。

因此,我们可以定义上下文无关文法语言,所以让g是一个上下文无关文法,有一个开始符号s,上下文无关文法语言为符号串,Alpha1至Alpha n,使得对于所有i,Alpha i是g的终结符,好的,所以t。

这里是g的终结符集,S为开始符号,S经过0或更多步到Alpha1,抱歉,Alpha1至Alpha n,好的,那么这说明了什么,这只是说所有终结符串,从开始符号开始,这些是语言中的字符串。

因此,终端来自事实,一旦终端包含在字符串中,没有规则可以替换它们,一旦终端生成,它是字符串的永久特征,在编程语言和上下文无关文法应用中,终端应该是我们建模的语言的标记。

考虑到这一点,让我们为cool片段写一个上下文无关文法,我们之前讨论过cool表达式,但一种可能的cool表达式是if语句或if表达式,回忆一下,cool的if语句有三个部分,并以关键字fee结束。

这有点不寻常,所以,嗯,看着这个,看着这个特定规则,我们可以看到一些惯例,我们使用的大写字母是非终结符,在这种情况下,只有一个非终结符,但我们总是用大写字母写出来,终端符号是小写的,好的。

另一种可能性是,它可能是一个while表达式,最后一种可能性是,它可能是一个标识符id,实际上有很多,更多的可能性,表达式有很多其他情况,让我给你看一点符号,这样东西看起来可能会更好,我们有很多。

我们有很多相同的非终结符的生产,我们通常在语法中把它们分组,然后我们只在右侧写一次非终结符,然后写明确的替代方案,所以这实际上与写出expert箭头两次完全相同,但我们在这里。

这只是一种将这些三个生产组合在一起的方式。

并说pr是所有三个右侧的非终结符,让我们看看这个上下文无关文法语言的字符串,因此,一个有效的cool表达式只是一个标识符,这很容易看到,因为pr是我们的开始符号,我称之为expert,并且有一个生产。

它只说指向id,我可以直接取开始符号到一个终端字符串,单个变量名是一个有效的表达式,另一个例子是一个if表达式,其中每个子表达式只是一个变量名,这是一个完美的cool表达式结构,类似地。

我可以对while表达式做同样的事情,我可以取while的结构,然后替换每个子表达式为一个单一的变量名,那将是一个语法上有效的cool while循环,还有更复杂的表达式,例如。

这里我们有一个while循环作为if表达式的谓词,不是你可能通常想写的,但语法上完全正确,类似地,我也可以有一个if语句,或一个if表达式作为if内部if表达式的谓词,如此嵌套。

这样的if表达式也是语法上有效的。

让我们再做另一个语法,这次是简单的算术表达式,所以我们将有我们的开始符号,并且唯一的非终结符,将被称为e,那么可能性是什么,嗯,e可以是表达式的和,或记住这是e箭头的一种替代表示法,它只是说。

我将使用相同的非终结符进行另一个生产,我们有两个表达式的和,或者我们可以有两个表达式的乘积,然后我们可以有出现在括号中的表达式,所以括号表达式,只是为了简单起见,我们可以只作为我们的基础,简单案例。

简单的标识符,所以变量名,这是一个关于加法和乘法的简单语法,它是and在括号和变量名中,所以让我们看看这个语言的一些元素,例如,一个单一的变量名是一个很好的语言元素,Id加id也在这个语言中。

id加id乘id也是如此,如果我们也可以使用括号分组东西,所以我们可以说id加id闭括号乘id,那也是一个你可以使用这些规则生成的,等等,有许多,这种语言中有更多字符串。

上下文无关文法是向能够用解析器表达我们想要的东西迈出的一大步,但我们仍然需要其他一些东西,首先是一个上下文无关文法,至少就我们目前所定义的那样,它只给我们一个是或否的答案,是,是。

一个字符串是否属于上下文无关文法的语言,它不是,我们还需要一种构建输入解析树的方法,所以在那些属于语言的情况下,我们需要知道它是如何属于语言的,我们需要实际的解析树,不仅仅是是或否。

在字符串不属于语言的情况下,我们必须能够优雅地处理错误,并向程序员提供某种反馈,因此我们需要一种做这件事的方法,最后,如果我们有了这两件事,我们需要它们的实际实现。

以便真正实现上下文无关文法,在我们结束这个视频之前,还有一点要说明,上下文无关文法的形式很重要,工具通常对您编写语法的方式很敏感,虽然有许多方式可以为同一语言编写语法,但只有其中一些可能被工具接受。

正如我们将看到的,在某些情况下,有必要修改语法,以便工具能够接受它,实际上,这也经常发生在正则表达式中,但这种情况要少得多,因此,对于大多数正则表达式,您会希望编写工具能够很好地消化它们,那也不是真的。

那也不适用于任意的上下文无关文法。

P19:p19 05-03-_Derivations - 加加zero - BV1Mb42177J7

在这视频中。

我们将继续讨论解析。

以推导的想法,所以从开始符号,嗯,我们可以依次应用生产规则。

这产生了一个推导和一个推导,可以用不同的方式绘制,而不是作为线性替换序列,我们可以画成一棵树,若有非终结符x出现,则替换x时,可用规则左部表示,将x的子节点,设为替换x的规则左部,应用产生式时。

x变为y1至y n,将y1至y n,作为x的子节点加入树中。

示例说明,嗯,这是我们的算术表达式简单语法,考虑这个特定字符串,id乘id加id,现在我们要做的是,我们将解析这个字符串,我们将展示如何为该字符串生成推导,同时构建一棵树,就在这里结束。

这里有一个从e开始到感兴趣字符串的推导,每一步应用一个生产规则,这是对应的树,这称为解析树,这是这个表达式的解析树或输入字符串,让我们详细地走过这个推导,右边红色的是我们正在构建的树。

左边蓝色的是我们迄今为止采取的推导步骤,因此,最初我们的推导只包含开始符号,E和我们的树只包含根,所以最初我们的推导只包含开始符号,也是起始符号,所以第一步是,嗯,有产生式e->e+e,意思是,在树中。

我们取根节点,给它三个子节点,e+和e,所以现在替换第一个e,嗯,为e乘z,使用产生式e->e乘e,这意味着我们取树中第一个e,给它3个子节点,每个e一次,继续,我们取这里剩下的第一个e,用id替换它。

这意味着我们让id成为树中左起第一个e的子节点,然后用id替换第二个e,使用产生式e去id,最后我们对第三个e做同样的事,现在我们已经完成了解析树,所以再次,从开始符号到要解析的字符串。

我们感兴趣的是解析,过程中我们构建了这个表达式的解析树。

关于解析树有很多有趣的事情可以说,首先,解析树的叶子是终结符,内部节点是非终结符,此外,按顺序遍历叶子是原始输入。

让我们回到例子并确认所有这些,如果我们看叶子,我们可以看到它们都是终结符,好的,内部节点都是非终结符,在这种情况下,我们的语言中只有一个非终结符,所有内部节点都是e,叶子是字符串的终结符。

然后我们可以看到,如果我们按顺序遍历叶子,我们得到的就是我们开始时的输入字符串。

此外,解析树显示了操作与输入字符串的关联,而输入字符串没有,你可能已经注意到,这个解析树是如何构建的,乘法比加法结合得更紧,因为乘法是包含加法的树的子树,这意味着我们会先做e乘以e。

然后再加e和some,有些人可能会想,嗯,我是怎么知道要选这个解析树的,因为实际上,如果你考虑一下,还有另一种推导,实际上,有好几种推导会给我不同的解析树,其中加法或乘法靠近根部,加法嵌套在乘法中。

所以我们现在先不要担心这个,让我们只是说,我们以某种方式知道这是想要的解析树,我给了你一个产生这种解析树的推导。

在之前的推导中继续,我展示的实际上是一种非常特殊的推导,它被称为最左推导,哪里,每一步替换最左非终结符,存在自然等价的最右推导概念,这就是,又是相同字符串的最右推导,从开始符号开始,以字符串结束。

关注并注意每一步替换最右非终结符,这里替换唯一的非终结符e,得到e加e,第二步替换第二个非终结符e为id等,余下的字符串。

用小图完整展示树和推导,这里是树和推导同时,这里又是树,这是开始符号e的根,蓝色是推导,开始替换e为e加e,这是唯一的非终结符,所以是最右的,从树的右侧开始,替换右边的e为id。

然后左边的e被替换为e乘e,现在剩下的最右e被替换为id,最后剩下的e也被替换为id。

指出最右和最左推导,我展示的具有完全相同的解析树,这不是巧合,每个解析树都有一个最右和最左推导,只是分支添加的顺序不同,例如,如果我有第一个产生式,E去e加e,现在我有选择如何构建我的树。

我可以处理这个子树,或者我可以处理那个子树,如果我首先构建这个,那将是最右推导,如果我继续总是处理最右非终结符,当然,如果我首先处理这个,我可以用它来做最左推导,同样重要的是要意识到还有许多推导。

除了最右和最左,我可以,我可以以某种随机顺序选择非终结符进行替换,但最右和最左是我们最关心的。

所以总结一下,我们不仅对字符串是否属于特定上下文无关文法感兴趣,我们需要该字符串的解析树,推导定义了解析树,但通常一个解析树有多种推导,特别是我们关注最左和最右推导。

P2:p02 01-02-_Structure_of_a_Co - 加加zero - BV1Mb42177J7

讲座下半场欢迎回来。

继续编译器结构概述。

回顾编译器有五大阶段,词法分析,语法分析,语义分析,优化,代码生成,现在简要介绍每个阶段,解释编译器如何理解这些,类比人类理解英语。

理解程序的第一步,编译器和人类都要理解单词,现在人类可看例句,立即认出有四个单词,这是一个和句子,如此自动,甚至不思考,这里真有计算在进行,需识别分隔符,即空格和标点,像标点符号,还有线索,如大写字母。

这些帮助你分组,这些字母成可理解单词。

强调这不是简单事,看这句话,你能读,但需一点时间,因分隔符放错位,所以你可以看到单词就是单词,单词a和单词句子,但再次,这不是你立刻就能做到的,立即,实际上你得做些工作才能看到分割点,因为它们没给你。

以你习惯的方式。

词法分析的目标是将程序文本分成单词,或我们编译器中所说的标记,现在有一段程序文本示例,不是一段英文文本,我们过一遍并识别标记,有一些明显的关键词,像if和then和else要识别,然后是变量名。

像x和y和z,还有常数,像数字一和数字二,然后是一些运算符,双等号是一个,赋值符是另一个,这里已经有一个有趣的问题,我们如何知道双等号不是两个单独的等号,我们如何知道我们想要这是一个双等号,我们想要。

而不是两个单等号,好吧,我们现在不知道,但我们在词法分析实现的讲座中会讨论这一点,但我们还没有完成这个例子中所有标记的处理,要么还有一些,分号,标点符号也是标记,然后分隔符也是标记,所以这是一个空白。

是一个标记,这里又是另一个空白,是另一个标记,然后这里有大量的空白,用于分隔事物,比如关键字和变量名以及其他符号彼此,这些是这个例子的标记。

对人类来说,一旦理解了单词,下一步是理解句子的结构,这被称为解析,正如我们在小学都学过的,这意味着绘制句子,这些图表是树,这是一个非常简单的过程。

让我们看看这个例子,这行是一个较长的句子,解析的第一步是识别每个,嗯,单词在句子中的作用,所以我们有名词和动词和形容词等,但解析的实际工作是将这些单词组合成更高层次的构造,例如。

这个特定的句子由一个主体,一个动词,和一个宾语组成,好的,实际上这形成了一个完整的句子,所以这里我们有树的根,称为句子,并且被分解成组成部分,高级结构,如我们所说的是主体,动词,宾语,然后主体更复杂。

宾语也是如此,这是解析英语句子的例子。

解析英语文本和解析程序文本的类比非常强,实际上它们完全一样,这是我们的小示例代码块,让我们清晰地解析它,这是一个if else语句,因此,我们解析树图根将是,If then else。

这个if else由三部分组成,有一个谓词,一个then语句和一个else语句,现在让我们看看谓词,它由三部分组成,嗯,有一个变量,一个比较运算符和另一个变量,这些共同形成一个关系,比较。

两个东西之间的比较是一个有效的谓词,类似地,then语句由一个赋值组成,其中z得到1,else语句也具有赋值的形式,z得到2,所有一起,嗯,这是if else的解析树,显示其结构,将其分解为其组成部分。

一旦我们理解了句子结构,下一步是尝试理解所写的内容,这很难,实际上我们不知道人类是如何做到的,我们仍然不理解词法分析和解析之后发生了什么,我们知道人们确实进行词法分析和解析。

与编译器词法分析和解析程序的方式大致相同,但坦率地说,理解意义对于编译器来说太难了,关于语义分析,首先要理解的是,嗯,语义分析是编译器只能进行非常有限的语义分析,特别是。

编译器通常做的事情是尝试捕捉不一致性,因此,如果程序以某种方式自相矛盾,编译器通常可以注意到并报告错误。

但他们并不真正知道程序应该做什么,作为我们在语义分析中做的事情的一个例子,使用英语中的类比,让我们考虑以下句子,Jack说Jerry把他的作业忘在家里了,问题是,这里的his指的是谁。

它可以指Jerry,在这种情况下,我们会读,Jack说Jerry把Jerry的作业忘在家里了,或者它可以指Jack,在这种情况下,我们可以读句子为Jack说Jerry,把Jack的作业忘在家里了。

没有更多信息,我们其实不知道他指的是哪个,是Jack还是Jerry,更糟的是,看看下面这句话,Jack说Jack把作业忘家了,问题是这句话涉及多少人,嗯,最多可能有三人,可能有两个不同的Jack。

他甚至可以指完全不同的人,我们不知道,没有看到故事其余部分,所有可能为他,但可能只有一个人,可能是杰克和杰克,这种歧义是语义分析问题。

编程语言中的类比是变量绑定,因此我们会有变量,在这种情况下,一个名为杰克的变量,可能还有更多名为杰克的变量,遗嘱语言将非常严格,以防止歧义,如上一页英语句子中的歧义,所以你知道,在这个例子中。

问题是此输出语句打印什么值,答案是它将打印四,因为对jack变量的这种使用绑定于此定义,外部定义被隐藏,所以外部定义在此范围内不活跃,因被内部定义隐藏,这是许多词法作用域编程语言的常规规则。

现在编译器除了分析变量绑定外还执行许多语义检查,这里有一个英语示例,所以杰克把作业忘在家里,在通常的命名约定下,假设杰克是男性,我们知道杰克和她在类型上不匹配,所以我们知道无论她是谁,她不是杰克。

类似类型检查。

第四编译器,日常英语中,阶段优化没有很强的对应词,但有点像编辑,实际上,这很像专业编辑在必须缩短文章长度时所做的,以符合字数预算,例如,我有这个短语,有点像编辑,如果我不喜欢它,若觉得太长。

可替换'嗯',中间四词为两词,类似,现为,但类似编辑,与原句意相同,但用词更少,程序优化目标为,修改程序以减少资源使用,或许我们想用更少时间,希望程序运行更快,或许我们想用更少空间。

以便手持设备存更多数据,可能关注减少耗电量,若有外部通信,可能关注减少网络消息,或数据库访问次数,可能想减少多种资源。

嗯,优化程序使用,示例程序优化类型,编译器规则:X=Y*0=X=0,看似真改进,避免乘法,仅做赋值,节省计算,现在,不幸的是,这不是正确规则,这是编译优化需了解的重要事项之一。

那就是何时进行某些优化并不总是明显,或不,现在发现这条特定规则适用于整数,好的,若x和y为整数,则乘以零始终等同于赋值为零,但对浮点数无效,为何如此?因为需了解IEEE浮点标准细节。

IEEE标准中有个特殊数,称为非数字的NaN乘0仍为NaN,特别地,NaN乘0不等于0,若x和y为浮点数,不能做此优化,实际上,若做了此优化,会破坏某些,依赖NaN正确传播的重要算法。

最后编译阶段是代码生成,常称为代码生成,可产生汇编代码,这是编译器最常产生的内容,但总体上是一种翻译成其他语言,这与人类翻译完全类似,就像人类译者可能将英语翻译成法语,编译器将高级程序翻译成汇编代码。

总结来说,几乎每个编译器都有我们概述的五个阶段,然而,这些比例在过去几年中发生了很大变化,如果我们回到FORTRAN 1并查看该编译器内部,我们可能会看到大小和复杂性类似于这样的东西。

我们有一个相当复杂的词法分析阶段,嗯,一个同样复杂的解析阶段,一个非常小的语义分析阶段,嗯,一个相当复杂的优化阶段,另一个相当复杂的代码生成阶段,因此,我们看到一个编译器,复杂性是,嗯。

相当均匀地分布在整个过程中,除了它的语义分析,这在早期是非常弱的,今天,如果我们看一个现代编译器,你会看到词法分析中几乎没有什么,解析中也很少,因为我们有非常好的工具来帮助我们编写这两个阶段。

我们会看到一个相当复杂的语义分析阶段,我们会看到一个非常大的优化阶段,事实上,这是所有现代编译器的核心组件,然后是一个非常小的代码生成阶段,因为再次,我们非常了解这个阶段,这就是本讲座的内容。

未来的讲座将详细研究这些阶段。

P20:p20 05-04-_Ambiguity - 加加zero - BV1Mb42177J7

本视频将讨论,歧义语境。

自由文法,编程语言中的,及应对方法。

从表达式语法开始,加法和乘法标识符,仅看字符串id乘id加id。

该字符串有两个解析树,id乘id加id,使用该语法有两个解析树,先做左侧解析树,首先,从开始符号e开始,该推导的第一项必须是,e到e加e,e加e,然后替换最左边的e为e乘e,使用e到e乘e,还剩下加e。

此时可看到该解析树,完成这两项推导,解析树构建完成,其余推导生成这些id,共需3项推导,完成这些,将得到id乘id加id,没问题,现在切换到右侧解析树,或解析树右侧,同样从e开始。

但这次先使用e到e乘e,然后替换最右边的为e加e,得到e乘e加e,完成这两项推导,解析树构建完成,再次使用3项推导,可得到id乘id加id,可以看到,有两个推导产生两个解析树,明确来说。

每个解析树有多个推导,每个解析树有左推导,右推导和其他推导,这里是不同情况,有两个不同的解析树,每个解析树有多个推导,每个解析树有左推导,右推导和其他推导,这种情况不同,这里有两个解析树。

我们有两种推导,产生完全不同的解析树。

这就是歧义文法的标志或定义,如果一个文法对于某些字符串有多个解析树,那么它就是歧义的,另一种说法是,对于某些字符串,存在多个,最右或最左推导,因此,如果某个字符串有两个或更多个最右推导。

或两个或更多个最左推导,那么该字符串将有两个不同的解析树,该文法将是歧义的,歧义是不好的,如果某些程序有多个解析树,那么基本上意味着你让编译器选择,程序的两个可能解释中,你想要它生成代码的那个。

这不是一个好主意,我们不喜欢编程语言中的歧义,并将关于程序含义的决定留给编译器。

现在有几种方法可以从文法中消除歧义,最直接的方法,也是我们首先讨论的,就是重写文法,使其无歧义,即编写一个新的文法,生成与旧文法相同的语言,但每个字符串只有一个解析树,这是我们所看过的文法的重写。

让我们再次写下我们最喜欢的输入字符串,Id times id plus id,让我们看看根据这个新文法,该字符串将如何被解析,我们从开始符号e开始,现在注意,e不再生成加号或乘号,实际上。

我们现在将文法分成了两套产生式,两个非终结符,e控制加号的生成,e prime控制乘号的生成,好的,那么为了生成加号,我们必须使用e prime plus e的生产式,没有其他方法可以做到,现在。

如果我们看看e prime,e prime能做什么呢,e prime可以生成标识符乘以某些东西,如果你看看这个文法,那就是,唯一可以生成标识符乘以其他东西的生产式。

涉及乘号的唯一其他可能性是生成括号表达式,这显然不会匹配我们正在尝试解析的字符串,所以我们必须使用这个生产式,id times e prime,现在为了匹配字符串,我们可以看到,这个e prime。

第二个,剩下的这个e prime必须去id,因为在乘号和加号之间只有一个标识符,只有一种生产式能做到这一点,所以这是完全唯一和确定的,最后,我们这里有最后一个e,我们希望用它来产生一个id。

我们如何做好这件事,e可变为e',实际上它必须如此,然后e'可单独变为id,这样我们就成功解析了字符串,总的来说,这个语法做得如何,如我们所说,我们分层了语法,我们将产生式分为两类,一类处理加法。

一类处理乘法,每个运算符都有一个非终结符,e产生式控制加法的生成,让我们看看发生了什么,如果i e可变为e' + e,然后仅关注e产生式,这个e会发生什么,我们可以再做一次,e' + e' + e。

如果我们再做一次,我们将有e' + e' + e' + e,总的来说,我想你明白了,我们可以生成任意数量的e'相加,最后那个e,最终总是剩下一个e,当我们想要停止生成加法时,e必须重写为,e'。

最终我们将得到e'序列,由加号分隔,好的,这就是e'能做到的,抱歉,这就是e现在能做到的,现在让我们看看e'能做到什么,e',让我们只关注前两个产生式,因为我们能看到前两个产生式处理标识符乘以某物。

而后两个产生式处理括号表达式,但希望你看到对称性,对于标识符和括号表达式,这真的是同一个想法,我们只做标识符,以保持幻灯片不过于拥挤,e'可变为id乘以e',我们可以重复,我们可以变为id乘以。

Id乘以,E',我们可以再次重复,我希望你开始看到相同的模式,这与加法是相同的想法,最终我们得到的是一堆标识符相乘,好的,因为那个尾随的e',在这种情况下,最终,可重写为标识符,现在考虑e'的情况。

实际上有两种选择,我们可以生成标识符,或生成带括号的表达式,因此,对于这些标识符,它们本可以是带括号的表达式,所以让我在这里写一些带括号的表达式,只是为了提醒我们有另一种选择,最终我们得到一个由标识符。

或带括号的表达式组成的字符串,由乘号分隔,好吧,就这样,所以希望你能明白这是如何工作的,这是语法结构的方式,因为我们有两个独立的产生式组,所有加号必须在,乘号之前生成,乘号将在解析树中更深地嵌套。

然后加号,加号在最外层生成,然后e'将生成乘号内的加号,因此,语法强制乘号比加号绑定得更紧,这里值得指出的一个细节是注意,在带括号的表达式内部,我们有一个e,而不是e',为什么是这样呢?

一旦我们在带括号的表达式内部,括号的整个目的是显示明确的关联,这样我们才能通过使用括号在乘号内包含加号,这就是,这就是,这就是在语法中用e而不是e'的意义。

回顾一下,我们从一个语法和一个字符串开始,我们想解析id,乘以id加id,但该语法给出了该字符串的两个不同的解析树,这是两个解析树再次,通过重写语法,我们能够消除右边的解析树,因此,这个解析树不再可能。

左边的解析树被修改了,现在有更多的符号,我的意思是,例如,这个变成了e',这里有一个非终结符链,这个e变成了e',然后变成了id,这里还有一些其他的小改动,因此,解析树并不完全与之前相同。

但我们能够得到一个解析树,其中乘号比加号绑定得更紧。

让我们考虑另一个有趣的例子,如果-那么-否则表达式,其中else是可选的,这是编写该语法的其中一种方式,这里有一个if-then-else和通常的方式,但然后我们还有一个产生式,其中没有else。

这就是那种情况,其中else是可选的,然后可能还有其他类型的表达式,所以我们不需要关心那些,我们只关注,if-then-else部分。

问题是这种特定的if-then-else表达式有两种可能的解析树,一方面,else可能属于外层,如果这是特定的内层,if可能没有else,并且else将与外层相关联,另一种可能性是else属于内层。

如果这样的话我们有这种结构,if-then-else嵌套在,没有else语句的if-then中,在编程语言中我们想要的,我知道的是第二种形式,我们希望else与最近的匹配。

if-then相关联,而不是更远的,我们可以编写一个语法来表达所需的关联,我们想要的是每个else都应该匹配最近的未匹配,then所以每当我们看到一个else,我们希望它与最近的,then相关联。

该then没有更近的匹配else,这意味着if语句将被分为两类,将有匹配的if,那些拥有所有的,then语句嵌套在它们内部的,匹配了else和未匹配的if,其中一些,then-then表达式在未匹配的。

if内部没有匹配的else,那么匹配的if看起来像什么?这很简单,if-then-else是一个匹配的,if必须有both一个then和一个else和任何嵌套的,if语句在两个分支上。

都必须也有匹配的else's,如果你有任何其他类型的构造,不是if-then-else,那也是被认为是匹配的,if那么未匹配的if呢?一种可能性是它们它只是一个未匹配的,if那里没有else,所以。

我们有if和then,但没有匹配的else,如果有个if then else,嗯,所以顶层,if then有匹配的else,但未匹配的if在内部,那么我们可以说未匹配的if必须在else分支。

不能在then分支,then分支必须是匹配的if,为什么是这样呢,想想看,如果这个是一个未匹配的,如果这个是一个未匹配的,if,意味着这里有些then没有匹配的else。

然后这里的else会更接近那个then而不是这个end,然后根据我们的定义,它必须匹配那个,好的,这种情况是不可能的,所以未匹配的if的唯一可能性是它本身是一个if then else。

是那个if then else,在then分支上必须匹配,未匹配的if then else必须在else分支。

所以现在重新考虑我们之前的那个表达式,我们可以看到,这个else应该匹配这个then,所以关联应该是这样的,因此,右边的解析树不是我们想要的,我们之前给出的语法将按照,左边的解析树解析表达式。

你可能会认为无歧义的if then else语法复杂难懂,坦白说,我同意你的看法,不幸的是,不可能自动地将一个有歧义的语法转换为无歧义的语法,所以如果我们想要无歧义的语法,我们必须手动写出它们。

这确实会导致更复杂的语法和语法,比使用更直接的歧义语法更难阅读和维护,一个可能性是尝试以某种方式接受歧义,因为这将允许我们更自然的定义。

但是,然后我们需要某种消歧机制,我们需要某种方式来说明当我们有多个解析树时想要哪个,实际上,大多数实用的解析工具采用第二种方法,所以,我们没有重写语法,我们使用更自然的歧义语法。

工具提供了一些类型的消歧声明,最流行的消歧声明形式是优先级。

所以,我们没有重写语法,我们使用更自然的歧义语法,工具提供了一些类型的消歧声明,最流行的消歧声明形式是优先级。

关联性声明,这是加法的自然语法,嗯,在整数上,这是有歧义的,即使只有一个中缀操作,我们也能得到歧义,因为这个语法没有告诉我们加法是左结合还是右结合,这里的简单解决方案是有一个关联性声明。

所以我们声明加法是左结合的,这是bison使用的符号,所以bison是一个特定的工具,一个百分号左加声明加法是左结合操作,这样就会排除这个特定的解析树。

现在这是一个稍微更复杂的语法,基本上是我们开始时使用的语法,在这个视频的开头我们有加法和乘法在整数上,这个语法仍然是歧义的,因为它没有说明乘法相对于加法的优先级,解决这个问题的方法是使用多个关联性声明。

我们声明加法是左结合的,我们声明乘法是左结合的,然后加法和乘法之间的优先级由顺序给出,所以这里乘法出现在加法之后意味着乘法的优先级高于加法,警告一句,这些声明被称为关联性和优先级声明。

但这不是解析器内部真正发生的事情,解析器实际上并不理解关联性和优先级,相反,这些声明告诉它在某些情况下做出某些类型的移动,我们不会真正能够解释这一点,直到我们深入解析技术,但请小心使用这些声明。

通常它们表现得像你所期望的关联性和优先级,但在某些情况下它们会导致令人惊讶的行为。

P21:p21 06-01-_Error_Handling - 加加zero - BV1Mb42177J7

本视频,将稍微偏离主题,讨论编译器如何处理错误,和,特别是,解析器中可用的错误处理功能类型。

编译器有两个相对独立的工作,第一个是将有效程序翻译,即,如果它从程序员那里得到一个正确的有效程序,它需要为该程序生成正确代码,现在,与这项任务不同,提供错误程序良好反馈的工作,甚至仅仅是检测无效程序。

因此我们不想编译任何不是有效程序的程序,在编程语言中,编程语言有许多不同类型的错误,仅举几例,例如,我们可能有词法错误,即使用在语言中不存在的任何基本符号字符,这些将由词法分析阶段检测。

我们可能有语法错误,这些将是解析错误,当所有单个词法单元都正确时,但以某种方式组装,没有意义,我们不知道如何编译它,可能有语义错误,例如,当类型不匹配时,我声明x为整数并用作函数。

这些将是类型检查器的任务来捕获,然后实际上可能有很多错误,呃,在你的程序中,这些不是编程语言或你编写的程序的错误,实际上它是一个有效程序,但它没有做你打算做的事情,实际上你的程序有一个错误,因此。

虽然编译器可以检测许多种错误,它不能检测所有错误,你知道,一旦我们超越了编译器所能做的,然后测试人员和用户将找到程序中剩下的问题。

那么良好的错误处理要求是什么,嗯,我们希望编译器准确清晰地报告错误,以便我们可以快速识别问题并修复它,编译器本身应迅速从错误中恢复,所以当它遇到错误时,它不应该花很长时间来决定,在继续之前做什么。

最后我们不想错误处理减慢有效代码的编译,那就是,我们不应为错误处理付费,如果我们并不真正使用它。

我将谈论三种不同的错误处理方式,恐慌模式和错误产生是当前编译器中使用的两种,这些都是今天人们实际使用的东西,自动,局部或全局修正是一个过去广泛追求的想法,我认为它在历史上很有趣。

特别是与今天人们所做对比,以及为什么人们很久以前尝试这样做,恐慌模式是最简单、最流行的错误恢复方法,基本思想是当错误被检测到时,解析器将开始丢弃标记,直到找到一个在语言中有明确角色的标记。

然后它将尝试重新开始并从该点继续,这些标记,它正在寻找的,称为同步标记,这些只是语言中有明确角色的标记,因此我们可以可靠地识别我们在哪里,因此,一种典型策略可能是尝试跳至语句的末尾,或函数的末尾。

如果在语句或函数中发现错误,然后开始解析下一个语句或下一个函数。

让我们看一个简单的假设性恐慌模式错误恢复示例,这是一个表达式,显然,如果我们不应该有两个加号连续出现,那么这里就有问题,所以在第二个加号处出错了,解析器将过来,解析器将从左到右进行,它将看到左括号。

它将看到数字1,它将看到加号,一切都好,然后它将看到这个第二个加号,它将不知道该怎么办,它将意识到它卡住了,并且语言中没有两个加号连续出现的表达式,它需要做些什么来恢复,它遇到了解析错误。

它必须在这一点采取一些错误行动,所以在恐慌模式恢复中,它将做的就是按下恐慌按钮,所以就在这一点它将说放弃,我不再正常解析了,它进入了一个不同的模式,在那里它只是在扔掉输入,直到找到它认识的东西,例如。

可以说,这项政策,针对这类错误是跳过,到下一个整数,然后尝试继续,因此,它将忽略这个加号,然后它将从这里重新开始,期待看到另一个整数,尝试完成这个表达式,它将把此视为1加2,然后括号将匹配。

表达式其余部分将解析得很好,在如bison等工具中,广泛使用的解析器生成器,你可能使用的,嗯,项目,有一个特殊的终结符error,描述要跳过多少输入,bison中给出的产生式看起来像这样。

你可以说e的可能性是e可以是整数,它可以是两个e表达式的和,E的两个表达式,它可以是一个括号表达式,如果这些都不行,好的,这些都是正常生产,如果那些都不行,那么我们可以尝试这些有错误的生产。

空气是野牛的特殊符号,它说好吧,这是要尝试的替代方案,如果这些都不行,所以如果你发现错误,集中看这个,这说的是,解析时发现错误,好的,还没说如何工作,未来视频会看到,概念上解析器识别,这里。

状态是期待整数,或加号或括号表达式,如果这不起作用,如果卡住了,那就按紧急按钮,你可以宣布它处于错误状态,它可以丢弃所有输入,此错误将匹配到下一个整数的所有输入,然后整个东西可以算作一个e。

然后我们将尝试继续解析,类似地,我们可以扔掉整个东西,然后从括号边界重置,然后继续解析,这是两种可能的错误恢复策略,如果我们发现语法中特定符号的错误,你可以有错误,嗯,这些包含错误标记的生产,嗯。

对于语言中不同种类的符号,你可以有。

嗯,任意数量的,另一种常见策略是使用所谓的错误生产,这些指定了程序员经常犯的已知常见错误,就像语法中的备选生产,所以这是一个简单的例子,假设我们正在开发一种编程语言或编译器,为许多数学家使用的编程语言。

而不是像计算机科学家那样写五次x,这些家伙总是想写五,空白x,只是为了将五和x并排放置,看起来更像正常的数学符号,他们抱怨这总是给他们解析错误,解析器一直在抱怨,这不是一个有效的表达式,好吧。

我们可以直接进入语法并添加一个生产使其有效,我们可以说好吧,现在,它是合法的,如果我有那种表达式,只是将两个表达式并排放置在一起,没有中间的运算符,这有一个缺点,显然,如果我们这样做很多。

我们的语法会变得很难理解,维护起来也会变得很难,本质上,所有这些都是将常见错误提升为备选语法,但这种方法在实际中被使用,人们确实在做这样的事情,你会看到,例如,当你使用gcc和其他生产c编译器时。

它们通常会警告你不应该做的事情,但无论如何它们都会接受,这本质上就是它们实现这一点的机制。

最后我想谈一谈错误纠正,所以到目前为止,我们只讨论了检测错误的策略,但我们也可以尝试修复错误,也就是说,如果程序中有错误,编译器可以尝试帮助程序员并说,程序有错误,编译器可以尝试修复,哦,你知道。

你显然不是有意写那个,让我试着找给你一个程序,那个能运行的,还有这类修正,某种意义上我们想找附近的程序,与程序员提供的程序不太不同的程序,但我们无法正确编译,你可以做几件事。

人们尝试过的事情中有像插入和删除这样的东西,所以你想最小化编辑距离,那将是用来确定一个程序是否接近,程序员提供的原始程序的度量,你还可以在一定范围内进行穷尽搜索,尝试所有可能的,与提供的程序接近的程序。

这种方法有几个缺点,实际上有很多缺点,嗯,你可以想象这很难实现,实际上相当复杂,嗯 这会减慢正确程序的解析,因为我们需要保留足够的状态,以便我们能够管理这个搜索,或者在实际上遇到错误的情况下进行编辑。

当然附近并不是,它并不完全清楚,那意味着什么,各种附近的观念可能实际上不会,带我们到一个程序员,程序员实际上会满意的程序,最著名的错误纠正例子是编译器pc,这是一个pl one编译器,pl部分是。

c代表的是更正或康奈尔,这是编译器构建的地方,plc以愿意编译你给的任何东西而闻名,你可以给它,电话簿你可以,人们确实给了它像哈姆雷特独白这样的东西,它会打印出很多错误消息,有时这些错误消息会很有趣。

最终它会进行更正并产生一个始终有效的运行,pl one程序。

你可能会问为什么人们费心于那个那似乎,嗯,但那可能看起来并不那么有吸引力,嗯 对我们今天来说,你必须意识到当这项工作在二十世纪七十年代完成时,人们生活在一个非常不同的世界,有一个非常缓慢的重编译周期。

编译运行程序可能需整天,早上提交程序,下午可能才得到结果,这种反馈周期下,程序中一个语法错误都致命,可能因错过一个关键词而浪费整天,输入关键词时出错,编译器尝试找到可运行的程序,若改正小错误可节省整天。

若能修正小错误并运行成功,这实际上是有用的,目标是一次循环找出尽可能多的错误,他们会尝试,尝试找出尽可能多的错误以恢复,嗯,找出尽可能多的错误,提供尽可能好的反馈,这样可修正尽可能多的错误。

避免尽可能多的重试周期,甚至可能自动修正程序,这样可查看修正是否正确,然后可能返回的结果是有用的,这使你能够在下一轮之前进行更多调试。

现在情况完全不同,我们有非常快的,几乎交互式的重新编译周期,因此,用户通常不感兴趣找到很多错误,他们倾向于每轮只修正一个错误,编译器仍报告许多错误,会给出很多很多错误,但我的观察,当然也是我的习惯。

以及我看到许多人做的,只是修正第一个,因为它最可靠,并且是编译之前必须修正的,如果编译足够快,这可能是最高效的做法,因此,和,结果,复杂的错误恢复今天不如几十年前有吸引力。

P22:p22 06-02-Abstract_Syntax - 加加zero - BV1Mb42177J7

本视频中,将讨论编译器使用的核心数据结构,抽象语法树。

简单回顾,解析器跟踪标记序列的推导,但仅此对编译器并不十分有用,因为编译器的其余部分需要程序的某种表示,它需要一个实际的数据结构来告诉它程序中的操作是什么,以及它们是如何组合在一起的,嗯。

我们知道有一种这样的数据结构叫做解析树,但结果表明,解析树并非我们想要处理的,相反,我们想处理一种称为抽象语法树的东西,抽象语法树实际上就是解析树,但忽略了一些细节,我们从解析树的细节中抽象出来。

这里有一个你会看到的缩写,Asts代表抽象语法树。

让我们看看语法,这是语法,对于整数的加法表达式,我们还括起了表达式,这是一个字符串,词法分析后,我们有什么,嗯,我们得到了一个标记序列,带有相关词素,告诉我们实际字符串是什么,然后传递给解析器。

然后我们构建一个解析树。

这是该表达式的解析树,现在我要强调这种表示,解析树实际上完全适合编译,我们可以使用解析树做编译器,这是程序的真实表示,问题是那样做会很不方便,为了看到这一点,让我指出解析树的一些特征,首先。

你可以看到解析树很冗长,例如,我们这里有节点e,它只有一个子节点,当节点只有一个继承者时,这对我们有何作用,嗯,我们实际上不需要,E,我们可以直接将五放在这里,使树变小,类似地,对于其他单一继承者节点。

此外,这些括号这里,嗯,这些在解析中非常重要,因为它们显示了这些论点与这两个加法操作的关系,这表明这个加法是嵌套的,这个加法在这里,嵌套在这个上面的加法中,但一旦我们完成了解析。

树结构告诉我们相同的事情,我们不需要知道,这些在括号内,这两个表达式的事实是,这个加法的参数已经告诉我们所有需要知道的内容,因此,所有这些节点在某种意义上都是多余的,我们不再需要这些信息了。

因此,我们更喜欢使用称为提取语法树的工具,它压缩了解析树中的所有垃圾,所以这是一个抽象语法树或假设的抽象语法树,它将代表与前一页幻灯片上的解析树相同的内容,你可以看到这里,我们真的只削减了基本项目。

我们有两个加法节点,我们有三个参数,关联仅由哪个加法节点嵌套在另一个中显示,我们没有多余的非终结符,我们没有括号,一切都简单多了,你可以想象编写算法。

走过这样的结构比走过前一页幻灯片上的红色和复杂结构更容易,当然,再次,它被称为抽象语法树,因为它从具体语法中抽象出来,我们抑制了具体语法的细节,只保留足够的信息以能够忠实地代表程序并编译它。

P23:p23 06-03-_Recursive_Descen - 加加zero - BV1Mb42177J7

本视频将讨论,我们的第一个解析算法。

递归下降解析。

递归下降是一种自顶向下解析算法,你可能怀疑也有自底向上的解析算法,确实有这样的东西,稍后我们会讨论,在自顶向下解析算法中,解析树从顶部构建,从根节点开始,从左到右,因此。

终端将在它们出现在标记流中的顺序中出现,例如,如果我有这个标记流,这是我可能构建的假设解析树,这里的数字对应于解析树中节点构建的顺序,我们必须从根开始,这是首先发生的事情。

然后如果t2属于解析树中的这里,那将是接下来发生的事情,但如果下一个位置是非终结符,那将是数字三,然后如果它有孩子,那么最左边的,因为我们从左到右,将是第四个要生成的,然后假设数字四的两个孩子都是终端。

所以那将是接下来的两个输入终端,等等,接下来发生的事情是数字三的第二个孩子,然后是最后两个按左到右顺序出现的终端。

让我们考虑这个整数表达式的语法,让我们看看一个特定的输入,一个非常简单的,仅打开5关闭,现在我们要做的是,我们将使用递归下降策略解析这个,我不会实际向您展示任何伪代码或其他类似的东西,我只是要走过。

如何使用这个语法和递归下降算法解析,这个输入字符串,基本思想是,我们从非终结符开始,我们从根节点开始,我们总是按顺序尝试非终结符的规则,我们将首先从egos到t开始,如果那不起作用。

我们将尝试egos到t加e,所以这将是一个自顶向下的算法,从根开始,我们将从左到右工作,我们按顺序尝试生产,当生产失败时,我们可能需要做一些回溯以尝试其他生产。

有三部分,我们使用的语法,我们构建的解析树,最初只是解析树的根,最后是我们处理的输入,我们将指示我们在输入中的位置,通过这个大红箭头表示我们已读的输入量,它始终指向要读取的下一个终结符号。

要读取的下一个标记,因此我们从左括号开始,好的,语法中也可以看到高亮,更亮的红色表示要尝试的生产,我们将开始构建解析树,尝试生产 e 到 t,这意味着什么?这意味着将 t 作为 e 的子节点。

然后我们继续尝试构建解析树,记住从左到右和自顶向下,现在 t 是一个未展开的非终结符,它是唯一的未展开非终结符,所以我们必须处理它,我们将做什么?我们将尝试 t 的生产,因为我们还没有尝试过。

我们将尝试第一个,T 到 int,下一步是使 int 成为 t 的子节点,这就是我们的第三部分,现在我们实际上有一些可以检查的东西,我们能否检查,我们是否取得了进展,因此请注意,只要我们生成非终结符。

我们实际上并不知道我们是否走在正确的轨道上,我们无法检查,我们生成的非终结符是否会产生输入字符串,但是一旦我们生成一个终结符号,然后我们可以将其与下一个输入标记进行比较以查看它们是否相同。

在这种情况下不幸的是,它们不相同,我们在这里生成的 int 不匹配输入中的左括号,因此显然这个解析,这个解析策略或这个,我们正在构建的解析树不会成功,因此我们将不得不做,我们将不得不回溯。

这意味着我们将撤销一个或多个决定,我们将回到我们的最后一个决策点,看看是否有其他替代方案可以尝试,我们最后做出的决定是什么?我们决定使用t到int,这样我们可以撤销,然后我们可以尝试t的下一个产生式。

那恰好是t到n乘t,我们将使用该产生式扩展t,现在再次生成左边的终结符,所以现在我们可以与输入进行比较,不幸的是,int标记与n的打开标记不匹配,所以我们必须回溯,所以我们撤销了这个决定。

这使我们回到尝试t的替代方案,还有一个可能性,那就是t到(e),我们使用该产生式扩展t,现在我们可以比较标记(与输入中的(匹配,它们匹配,那很好,这意味着我们可能走上了正确的轨道,由于它们匹配。

未来我们做的任何事情都必须与不同的输入匹配,所以我们将输入指针前进,那么我们现在要处理什么?我们必须扩展这个非终结符e,我们将做我们之前做过的事情,我们只是将从第一个产生式开始,所以我们有e到t。

然后我们必须处理t,所以我们将选择t的第一个产生式,我们有t到int,现在我们可以比较int与输入中的int是否匹配,确实如此,所以我们再次前进输入指针,现在我们在哪里,还剩下什么?

我们已经进展到这一点,我们正在看那个左括号,它也匹配,所以它与输入匹配,现在我们已经匹配了解析树中的所有内容,我们的输入指针在字符串的末尾,所以这实际上是对输入表达式输入字符串的成功解析。

所以这意味着我们接受。

P24:p24 06-04-_Recursive_Descen - 加加zero - BV1Mb42177J7

欢迎回到本视频,我将概述递归下降解析的通用算法。

在深入递归下降解析算法细节之前,让我先定义一些将在整个视频中使用的几个小东西,标记将是一个类型,我们将编写代码,标记将是所有标记的类型。

我们将在示例中使用的特定标记是诸如into、open和close之类的东西,对于加号和乘号,因此标记是一个类型,而这些是实例,该类型的值,然后我们将需要一个名为next的全局变量。

它指向输入字符串中的下一个标记,如果你记得上一视频,我们使用一个大箭头指向输入,指示我们的当前位置,全局变量next将在我们的代码中扮演相同的角色。

那么让我们开始,首先我们将定义一系列布尔函数,我们必须定义的一个函数是匹配输入中给定标记的函数,它是如何工作的呢?它接受两个参数,一个标记,好的,这又是标记类型。

然后它只是检查它是否与输入流中当前指向的内容匹配,所以t k是否等于next指向的东西,请注意作为副作用,我们递增next指针,然后返回的,然后是一个布尔值,这是真或假,是的。

我们传入的标记与输入匹配或否,它不匹配,再次,只是为了强调这一点,请注意next指针被递增,无论匹配是否成功或失败,现在我们需要检查的是s的第n个产生式的匹配,这是特定非终结符s的特定产生式。

我们将用返回布尔值的函数表示它,它被写作s下标n,所以这是一个只检查s的一个产生式成功的函数,我不会写出那个代码,我们一会儿会看到,然后我们需要另一个函数来尝试s的所有产生式,这个将被简单地称为s。

没有下标,没有下标,所以这一个会成功,若任何s的产生式能匹配输入,好吧,因此对于每个非终结符,我们会有两类函数,一类是每个产生式一个函数,它检查该产生式是否能匹配输入。

然后一类是将特定非终结符的所有产生式组合在一起。

并检查是否有任何一个能匹配输入,好的,这就是现在的总体计划,看看这些特定产品如何运作,我们将使用与上一视频相同的语法,该语法的第一个产生式是e->t,然后我们要做的是。

我们想编写函数来决定这个产生式是否匹配某些输入,这个恰好非常简单,很容易看出原因,首先我们编写e1函数,这是处理第一个e产生式的函数,仅在成功匹配某些输入时返回真,如果这个产生式成功匹配某些输入。

这个生产如何匹配输入,仅能匹配部分输入,若某些t生产匹配输入,我们为该函数命名,这就是函数t,尝试所有t的不同生产,因此,e一成功返回真,仅当t成功返回真,这就是第一部分的全部,对于第二个生产。

我们现在有更多工作要做,E将成功,若t加e能匹配输入,那如何运作良好,首先t要匹配输入,因此t的一些产生要匹配输入的一部分,在那之后,我们得在输入中找到加号,跟在t匹配的之后,若加号匹配。

那么e的一些产生要匹配输入的一部分,注意短路双的使用,在这里,实际上,这很重要,你在利用双&和c的语义,C++,它按从左到右的顺序评估双&的参数,所以首先,T将执行并注意到t的副作用,在输入指针上。

所以它正在增加下一个指针,它精确地增加,然而远t能走多远,所以无论t设法匹配什么,下一个指针将前进那么远,当此函数返回时,它将指向t未匹配的下一个终结符,那需要是一个加号。

对term的调用将再次增加下一个指针,这正是e应该接续的地方,e能匹配的任何内容都将增加下一个指针仅超出那个,以便语法之外的其他部分可以匹配它,注意这个特定函数被调用e两次,因为这是第二个生产的函数。

关于e,我们还要处理关于e的一件事,那就是函数e本身,我们需要编写一个函数来匹配e的任何替代方案,由于只有这两个产生式,它只需要匹配这两个产生式中的一个,这就是回溯处理的地方,现在唯一需要担心的状态。

在回溯中是下一个指针,因此,如果我们需要撤销决定,它需要被恢复,因此我们实现的方式是,我们在这个函数中有一个局部变量,名为Save的变量,记录下一个指针的位置,在我们做任何事情之前。

所以在尝试匹配任何输入之前,我们只记得这个函数被调用时下一个指针开始的位,现在要做的,进行备选匹配,我们首先尝试e一,看它是否成功,如果它失败,实际上让我们做成功的事,先看案例,如果这成功。

如果这返回真,那么双或的含义是,我们不会评估e二,所以这不会被评估,这里的第二个组件不会被评估,如果e一如果e一返回真,它会短路,因为它知道它会是真,无论什么,它将停止并注意,下一个指针将被保留。

我们将记住,当返回真时,下一个指针将指向未消耗的输入,现在考虑e一返回假的情况,如果e一返回假,或为真,仅当第二部分为真,我们首先做什么,好的,在我们尝试e2之前,如果e2返回真,那么整个表达式返回真。

并且e函数成功,如果e函数失败,那么对于e我们没有其他选择,失败将返回给推导中的更高层次的生产,它将不得不回溯并尝试另一个替代方案,最后,关于这个特定语句,下一个等于保存此处,这并不严格需要,注意。

这里我们在同一个变量中保存下一个指针,然后第一件事,然后我们做的第一件事是将其复制回下一个,这只是为了统一,使所有生产看起来相同,但由于这是第一个生产,我们实际上不需要这个赋值语句。

如果我们不想有它,所以让我们把注意力转向非终结符t,有三个生产,第一个是t去int,这是一个简单的编写,我们只需要匹配终端int,所以输入中的下一个必须是整数,如果是,那么t1成功,嗯,t2稍微更复杂。

那是生产int times t t去n times t,所以我们必须在输入中匹配一个int,然后是times,然后是匹配任何t生产的任何东西,第三个生产是t去open paren。

E close parenend,所以必须发生什么,我们首先必须匹配一个open end,然后匹配e生产的任何一个东西,所以我们在那里调用函数e,最后是一个close paren。

然后将这三个放在一起在函数t中尝试所有三个替代方案,我们就有和为e尝试所有三个替代方案时完全相同的结构,所以我们保存当前的输入指针,然后尝试替代方案是t1,t2,和t3按顺序,在每个步骤中。

我们在尝试下一个替代方案之前恢复输入指针。

要启动解析器,我们必须初始化下一个指针,指向输入流中的第一个标记,并且我们必须调用匹配任何从开始简单派生的东西的函数,所以在这种情况下,那就是函数,E,递归下降解析器手工实现很容易。

实际上人们经常手动实现它们,只需遵循我之前幻灯片上展示的纪律。

结束这个视频,让我们通过一个完整示例,这是我们的语法,这是递归下降解析器为该语法的所有代码,这是我们将要查看的输入,我们将仅标记下一个指针,指向输入中的初始is标记,同时我也会画出我们正在构建的解析树。

我们从调用开始符号开始,我们将尝试从e推导一些东西,我们首先会做的是尝试第一个生产,所以我们将尝试e1,e1会做什么,e1将尝试t,它将尝试从t推导一些东西,因此可能的解析树看起来像这样。

然后我们调用t,t会做什么,将按顺序尝试t的所有三个生产,它将调用t1,我们将看到t1将会失败,因为它将尝试一个int,我不会把它放入解析树中,因为它不会起作用,但int不会匹配左括号。

所以那将返回false,这将导致回溯,它将重置,呃,输入指针,好的,并回到字符串的开头,然后它将尝试t2,t2也会问,输入指针是否等于int,我们回忆一下,这里的term函数总是增加输入指针。

实际上这个指针将移动一个标记,但这将返回false,因为int不匹配左括号,所以我们将回到这里,输入指针将恢复到字符串的开头,然后它将尝试替代t3现在当我们最终到达t3时,一些好事将会发生。

首先它将做的是将询问,输入中的第一个东西是否是左括号,实际上它是,因此输入指针将前进,呃,指向int,然后它将尝试匹配可从e推导的东西,现在我们有了第一次递归调用,我们回到e这里。

它将尝试先e一然后e二,所以它调用e一,e一只能匹配,如果能匹配t,好的,现在我们在e内部,然后我们要调用t,t会做什么呢,它将按顺序尝试t的所有三个产生式,第一个恰好是单个标记符int,这将匹配。

它将调用,术语int t一正在调用术语结束,这匹配输入流中的下一个标记,我们对此感到高兴,输入指针再次前进,现在我们将通过所有这些级别的调用返回,嗯,t一成功,这意味着t成功,嗯,这意味着e成功,好的。

现在我们回到t三的产生式,我们要问,我们在输入中看到的下一个东西是否是n的闭合,确实如此,因此将记录闭合的n,现在t三将成功,这意味着t成功,这个t成功,最后我们回到根调用e,它返回真。

这意味着部分成功,加上我们现在处于输入的末尾,没有更多的输入要消耗,我们从开始符号返回真,因此我们成功解析了输入字符串。

P25:p25 06-04-1-_Recursive_Desc - 加加zero - BV1Mb42177J7

本视频中,将讨论递归下降算法的局限性。

上次,上次的语法,再次展示实现,一组相互递归的函数,考虑解析输入int时,最简单的输入字符串,嗯,逐步分析,我们首先实现非终结符e的所有产生式,接下来我们要做的是,这里我们调用e,然后它会尝试调用e1。

e1会做什么,e1会调用t,因为,当然,第一个产生式是e->t,让我们看看t做了什么,嗯,t将尝试生产,t1,好吧,t1会做什么,t1识别一个整数,好的,这样很好,会匹配并返回,好的,然后e会返回。

我们将成功解析,我忘了提,还有,在过程中,输入指针将在int上移动,完成后他将返回,我们将成功解析字符串int,因为e返回真,生产e返回真,我们消耗了所有输入,好吧,现在让我们考虑一个稍微复杂点的例子。

好的,那么让我们尝试输入字符串int times int,好吧,所以再次,我们从生产e开始,好吧,我们首先会做的是,我们会尝试生产e一次,和上次一样,第一个将调用函数t,再次是int生产,好的。

输入指针当然在这里,然后尝试匹配int,好的,如果尝试匹配输入流中的第一个标记,终端int,它将成功,好的,输入指针将移动,t1将返回真,因此结果为,函数t的右侧也会成功,因为t1返回真。

所以t将返回真,好的,因此e1将返回真,e e,返回真将导致e返回真,实际上程序将终止,E将返回真,输入指针仅前进到int,因此我们将拒绝解析,这实际上被拒绝了,问题是,当然,发生了什么事?

我们成功解析了这个输入吗?这显然属于该语法的语言,嗯,这里的故事实际上很有趣,当我们发现int匹配了t的第一个生产时,我们说t完成了,好的,t成功并匹配了输入,然后当e最终返回时,整个解析失败。

因为我们没有消耗输入,我们没有方法回溯并尝试t的其他替代方案,如果我们想要成功,我们得说,哦,好吧,即使我们找到了一个匹配部分输入的t的生产,由于整体解析失败,那一定不是为t选择的正确生产。

也许我们应该尝试t的其他生产,事实上,如果我们尝试了t的第二个生产t2,我们将匹配int times t,然后我们可能就会成功,我们成功匹配了in times end,好的,这里的问题是。

即使在一个生产中存在回溯,当我们试图找到一个适用于给定非终结符的生产时,因此,对于非终结符存在回溯,在我们试图找到一个适用于该非终结符的生产时,一旦我们为非终结符找到了一个成功的生产。

所以一旦非终结符承诺并返回,并说我已经找到了一种解析部分输入的方法,使用我的一个生产,没有回溯,这种结构无路可走,该算法无法回溯重选决定,尝试不同的生产,好吧,问题是,如果非终结符x的生产成功。

无法回溯尝试x的不同生产,一旦x的函数返回,我们真的就决定了这个生产,现在,这意味着我上次视频中展示的特定递归下降算法,不是完全通用和递归的,递归下降是一种通用技术。

存在可以解析任何语法的递归下降解析算法,可以实现任何语法的完整语言,它们具有比我上次展示的算法更复杂的回溯,现在,展示这个特定算法的原因是它容易手工实现,这实际上是一种递归下降算法或方法。

虽然它有这种限制,如你所见,它非常机械和直接,为给定的语法设计解析器,它将适用于相当大的正确语法类,特别是,它将适用于任何语法,对于任何非终结符,最多一个生产可以成功,所以如果你知道从你构建语法的方式。

语法可以进入的任何情况,或递归下降算法在解析过程中可以进入的任何情况,最多一个生产可以成功,那么这种语法,这种解析策略将是足够的,因为一旦你找到一个成功的生产,将永远不需要回去重选那个决定。

因为一定是其他所有生产都无法成功,结果是我们正在工作的示例语法,在最近几个视频中实际上可以编写为与这个算法一起工作,好吧,我们将不得不左因子化语法,但实际上有不止一种方法重写语法。

以与这个递归下降算法一起工作,一种方法是左因子化它,我不会在这个视频中再说更多关于左因子化的事情。

P26:p26 06-05-_Left_Recursion - 加加zero - BV1Mb42177J7

本视频将讨论,递归下降解析的主要困难。

称为左递归的问题。

考虑一个仅含一个产生式的简单文法,s 后跟 a,该产生式的递归下降算法如下,我们只需一个名为 s1 的函数,用于 s 的第一个产生式,如果 s 函数成功,然后,输入流中看到终端 a。

需要为符号 s 编写函数,由于只有一种选择,s 只有一个产生式,无需担心回溯,s 仅在 s1 成功时成功,仅有一种可能,现在你能看到问题所在,会发生什么,解析输入字符串时,将调用 s 函数。

它将调用 s1 函数,s1 函数将做什么,首先将调用 s 函数,结果,s 函数将陷入无限循环,永远无法解析任何输入,将始终陷入无限循环,该文法表现不佳的原因,是因为它是左递归的,左递归文法是任何具有。

从该非终结符开始,进行非空序列重写,注意 + 号,必须进行多次重写,如果执行一系列替换,回到左侧仍为相同符号的情况,这对解析不利,对于上面的文法,会发生什么,s 到 s a。

到 s a a 到 s a a a 等,总能达到,字符串以 a 结尾,s 在左侧的情况,如果字符串左侧始终是 s,将永远无法匹配输入,因为匹配输入的唯一方法是,首先生成终端符号,如果首先是非终结符。

将永远无法取得进展,这意味着,递归下降不支持左递归。

递归文法,这似乎是递归下降解析的一个主要问题,确实是个问题,但正如我们稍后所见,其实并不那么严重,所以让我们考虑一个稍微更通用的左递归文法,现在我们有两种产生式,对于s,s生成s后跟某个alpha。

或生成其他东西,未提及s,暂称其为β,考虑生成这种语言的规则,它将连接所有以β开头的字符串,然后跟随任意数量的α,但它以一种特殊方式进行,所以如果我写出一些推导,其中我使用了一些。

其中我使用了第一个产生式,几次你可以看到发生了什么,所以得到s变为s后跟α,然后s变为s后跟αα,然后s变为s后跟ααα,若我重复此操作,得到s后跟任意数α,然后在一步中可加入β,得到β后跟任意数α。

这就是生成该语言的证明,以β开始的语言,包含一些α序列,但可见它是从右向左完成的,首先产生字符串的正确部分,实际上它产生的最后一件事是输入中出现的第一件事,这就是为什么它不能与递归下降解析一起工作。

因为递归下降解析希望首先看到输入的第一部分,然后从左到右工作,而这个语法是为了从右到左生成字符串而构建的,这就是解决问题的想法所在,因此我们可以生成完全相同的语言,从左到右生成字符串而不是从右到左。

我们这样做的方法是用右递归替换左递归,需在此处添加一个符号,不再让s指向含s的左侧,而是让s指向β,生成第一个元素,注意,在第一个位置,然后指向s',s'是什么,做得好,s'产生预期的α序列。

也可能是空序列,如果你写出,嗯一些,嗯,你知道一个例子推导,我们有s到贝塔s撇,现在,使用s撇到贝塔阿尔法的规则,s撇到贝塔阿尔法阿尔法s撇到安,任何数量的改写。

我们得到贝塔后跟一些阿尔法的序列后跟s撇,然后在一步中我们使用epsilon规则,我们最终得到贝塔,后跟一些阿尔法的数量,所以你可以看到它生成与第一个语法完全相同的字符串。

但它是以右递归的方式而不是左递归的方式,一般来说我们可能有多个产生式,嗯其中一些是左递归的,一些不是,这个特定形式的语法产生的语言,嗯,将是所有从asset派生的字符串,从贝塔开始。

所以这里的一件事是不涉及s,然后继续零个或多个阿尔法的实例,我们可以做完全相同的把戏,这只是我们之前想法的概括,我们只有一贝塔和一阿尔法到许多贝塔和许多阿尔法的。

使用右递归重写这个左递归语法的通用形式在这里给出,所以这里每个贝塔都作为,第一个位置的选择,我们只需要一个额外的符号,S撇然后s撇,嗯,规则负责生成任何序列的阿尔法。

I's,现在事实证明那不是最通用的左递归形式,甚至还有一些,其他方式在语法中编码左递归,这里是一种重要的方式,所以我们可能有一个语法,所以如果你看这里,你看s甚至没有出现在右边,如果你看这条产生式。

A没有在任何地方出现在右边,所以这里没有所谓的立即左递归在这个语法中,但另一方面有左递归因为s到a阿尔法,然后a可以到s贝塔,所以我们在两步内,产生另一个以s开头的字符串,所以这仍然是一个左递归语法。

我们只是通过在左端插入其他非终结符延迟了它,在我们回到s之前,因此递归也可以消除,实际上这可以自动消除,甚至不需要人工干预,如果你看任何教科书,特别是在龙书中,嗯,你会发现做这些的算法。

因此,我们关于递归下降解析的讨论,嗯,它是一种简单而通用的解析策略,你可以使用递归下降解析任何上下文无关文法,因此,它在这一点上非常通用,它不能与左递归文法一起工作,因此,你必须消除左递归,现在。

原则上,这可以自动完成,你可以有算法来消除实际的左递归,人们手动消除左递归,原因是你需要知道你使用的语法,以便你可以编写语义动作,我们还没有讨论语义动作,但我们将很快看到它们,嗯。

因为你需要确切知道语法形式,语法具有人们通常自己消除左递归,但这并不难做到,事实上,递归下降在实际中是一种流行的策略,许多更复杂的生产编译器,实际上,使用复杂的文法使用递归下降,因为它非常通用,所以。

例如。

P27:p27 07-01-_Predictive_Parsi - 加加zero - BV1Mb42177J7

本视频中,我们将继续讨论自顶向下解析算法,使用另一种称为预测解析的策略。

因此,预测解析类似于递归下降,它仍然是一种自顶向下解析器,但解析器能够预测将使用哪个产生式,它从不鲁莽,解析器总能正确猜测,哪个产生式将导致成功的解析,如果有任何产生式将导致成功的解析。

它通过两种方式做到,首先,它查看接下来的几个标记,所以它使用前瞻来尝试确定应使用哪个产生式,因此基于输入流中即将出现的内容,但同时也限制了语法,所以这仅适用于,呃,受限形式的语法,优势是没有回溯涉及。

因此解析器是完全确定的,嗯,现在它永远不会尝试其他选择,预测性解析器接受称为LLK文法的东西,这是一个非常神秘的名称,所以让我解释一下,嗯,第一个l代表从左到右扫描,这意味着我们从输入的左端开始。

从左到右读取,我们讨论的技巧首都有l,第二个l代表最左推导,因此我们构建最左推导,意味着我们始终处理解析树最左非终结符,k代表k个前瞻符号,实际上,呃,尽管理论适用于任意k,实际上,K总是等于1。

实际上我们只讨论k等于1的情况,在这些视频中回顾递归下降解析,每一步。

可能有很多生产规则可供使用,因此我们需要使用回溯来撤销l型解析器中的错误选择,在每个步骤中,只有一个解析器,将只有一种可能的生产规则可供使用,那意味着什么?这意味着如果我有输入字符串。

如果我有解析器的配置,其中有一些终结符号,欧米茄和非终结符a,你知道,可能现在接着其他东西,可能有终结符和非终结符,但再次a是左最非终结符,下一个输入,是一个标记t好吧,那么恰好有一个产生式。

呃a去阿尔法,呃在输入t,好的,只有一个可能的使用生产,任何其他生产都保证是错误的,现在可能是即使a去阿尔法也不会成功,可能是我们会在一个情况,没有生产我们可以使用,但在一个l一解析器中总是最多一个。

呃,我们可以使用,所以在这种情况下我们会选择,呃,重写字符串到欧米茄阿尔法贝塔。

让我们看看我们最喜欢的语法,我们为最后几个视频一直在使用的,我们可以看到使用这个语法进行预测解析的问题,看看t的前两个生产,它们都以int开始,所以如果我告诉你下一个终端在输入流中,当我们解析时。

是一个整数,这并不能真正帮助你区分这两个生产,并决定决定使用哪一个,所以,事实上,只有一标记的看前,我不能在这两个生产之间选择,而且这不是唯一的问题实际上,所以我们有t的问题,但相同的问题也存在于e。

我们可以看到这里e的两个生产都以非终结符t开始,我们真的不知道该怎么做,因为t再次是一个非终结符终端,我们甚至如何做预测,但事实是他们以相同的东西开始,呃表明基于仅一个标记的看前。

对我们来说预测使用哪个生产并不容易,所以在这里我们需要做的是我们需要改变语法,这个语法实际上不适合预测解析,或至少对于ll一解析。

我们需要做一些被称为左因子化的语法,左因子化的想法是消除一个非终结符多个生产的一个共同前缀,所以这是一个口胡,让我们举个例子,让我们从e的生产开始,让我们开始于e的生产,我们可以看到。

e的两个产生式都以相同的前缀开始,相同的前缀,我们要做的就是提取这个公共前缀到一个单独的产生式中,我们将有一个e到t的产生式,然后我们将有多个后缀,因此,让我们引入一个新的非终结符x来处理其余部分。

所以这里我们有e到tx,它说e产生的所有内容都以t开始,这与这两个产生式是一致的,现在我们必须为x写另一个产生式来处理其余部分,那会是什么?一种可能性是我们处于这个产生式中,我们需要有一个加e。

然后在这个产生式中什么都没有,所以很容易处理,我们为x写一个可能,是它到加e,另一个可能性是它到epsilon,现在你可以看到一般想法,我们提取公共前缀,我们有一个处理前缀的产生式,然后我们写,然后。

我们引入一个新的非终结符来处理不同的后缀,然后我们只有多个产生式,一个用于每个可能的后缀,你可以看到这将做什么,这实际上将推迟关于使用哪个产生式的决定,而不是立即决定要为e使用哪个产生式,在这个语法中。

我们等到已经看到了t,无论t派生自什么,然后我们必须决定生产式的其余部分是加e还是空字符串,让我们做另一组产生式,所以我们有t um到,现在我们要消除的共同前缀是int。

所以我们将只有一个以int开始的产生式,然后我们将有一个新的非终结符来代表各种可能的后缀,现在这里我们还有另一个与它无关的产生式,所以我们只是把它留下,那个产生式就留在这里,因为它已经以不同的东西开始。

我们不会在这两个可能的生产式之间遇到任何麻烦,这两个可能的生产式,现在我们必须写y的生产式,我们再次只取我们留下的产生式的后缀,并将它们作为备选方案写下来,一个是空,另一个是times t。

所以我们最终得到了times t,或。

所以这是现在整齐地打出的左因子语法,我们使用这个语法来构建解析表。

现在别担心如何得到这张表,我不会给出算法,嗯,现在,嗯,但假设我们以某种方式得到了它,我将解释如何使用这张表,表的一维是解析树中的当前最左非终结符,所以它在行上,然后列代表下一个输入标记。

所以输入流中的下一个标记,然后条目是产生式的右侧,所以在那个解析点我们应该使用哪个生产。

那就是预测的生产,让我们举个例子,让我们看看e int条目,这个条目在这里,这表示当当前非终结符是e,意味着,解析树中的最左非终结符,并且下一个输入是整数,我们看到的,那么应该使用生产e去tx。

所以应该扩展e,嗯,用孩子tx。

嗯,让我们再做另一个例子,所以当当前非终结符是y,嗯,当前标记,当前输入是加号,那么应该使用生产y去epsilon,好的,这表示与上一个情况有点不同,它说当你看到一个加号在输入中。

并且你当前的最左非终结符是y,解析成功唯一的办法是y不产生任何东西,你需要去掉y并继续到另一个非终结符,无论哪一个在你去掉y之后是最左的,如果你想对这个特定的字符串有任何希望解析。

最后注意很多条目是空白的,那些是错误条目,让我们看看e星条目,这表示如果最左非终结符是e,并且下一个输入标记是时间符号,一个星号,嗯,那么,你不能做任何移动,对于e没有你可以使用的生产。

能够成功地解析那个输入,这就是你会引发解析错误的地方。

在本视频的其余部分,我将给出使用解析表的解析算法,然后在未来的视频中我们将解释如何构建解析表,使用解析表的解析方法类似于递归下降,除了最左非终结符s,我们看下一个输入标记a,如示例所示,我们查找表。

使用低处的生产,在,在,在s,a条目,而不是使用递归函数追踪解析树,我们将有一个记录前沿的记录栈,在解析树的任何一点,我们会有一些尚未展开的非终结符,它们总是在前沿,解析树的当前叶节点。

还有一些我们尚未匹配的终结符,它们都将记录在栈上,栈的重要性质是,最左边的待定终结符或非终结符,总是在栈的顶部,因此,我们试图匹配的终结符,或我们试图展开的非终结符总是在栈的顶部,如果我们达到错误状态。

我们将拒绝,因此,如果我们查找到表中的空条目,我们将拒绝该字符串,并且当我们到达输入的末尾且栈为空时,我们将接受,意味着我们没有待匹配的未展开终结符或非终结符。

所以这是算法,我们将栈初始化为仅开始符号s和特殊符号,美元符号,美元符号不是字母表的一部分,或者你可以认为,我们扩展了我们的字母表,添加了一个名为美元符号的新符号,美元符号标记栈的底部。

你也可以认为它标记输入的结束,所以这只是一种记录输入结束位置的方式,所以一旦我们匹配了s上的某些东西,那么剩下的最好在输入的末尾,这就是栈所表达的,现在我们在循环中,我们将重复以下动作。

直到我们无法重复它们为止,直到栈为空,好的,有两种可能的动作,让我们先做终结符,所以假设栈的顶部是终结符,所以我们将栈分为栈的顶部,和栈的其他部分,如果栈的顶部是终结符,我们将做什么,嗯。

我们将尝试匹配输入,所以我们将说,在栈的顶部,栈顶的终结符与输入的下一项匹配,然后我们前进输入并弹出栈,因此我们成功匹配了输入与终端,终端处理完毕,应进入栈中,匹配尚未处理的下一个项,如果他们不匹配。

若当前查看的终端与输入的下一个项不匹配,嗯,那是错误,这里没有回溯,无法解析字符串,将引发错误,第二类移动处理非终端,假设栈顶是非终端X,记住栈顶将是非终端,恰好是左侧非终端,现在我们查看解析表,呃。

在X和下一个输入符号的条目下,应该给出产生式的右侧,好的,现在我们弹出X栈,并将X解析树中的子节点推入栈中,这是扩展X的方式,现在解析中未处理的左侧项将是Y1,因为那是X的第一个子节点。

然后X的其他子节点紧随其后,然后是栈中其他内容,同样,如果当前非终端和输入在表中无条目,则是错误,解析停止,所以让我们通过一个例子。

呃,使用我们的解析表,你可能想回头查看解析表,我没有在这里包含它,因为空间不够,但我会通过它,你可以回头看看,并确信我做出了正确的移动,所以最初我们的栈是e美元符号。

所以使用我们的开始符号和美元符号是我们的输入结束符号,我们的输入将尝试解析int times int,那就是我们想解析的,当然我们还有新的符号,美元符号,我们将它附加到输入的末尾,如果一切顺利。

栈上的美元符号将匹配,输入末尾的美元符号再次,美元符号在这里只是标记输入的结束,美元符号只是一个标记输入结束的方式,表达我们需要解析整个输入,现在如果你查看e int条目。

所以第一个终结符和输入中的下一个终结符,以及我们解析中的最左终结符,你会看到我们应该采取的行动是使用生产式,E去tx,同时在这里构建解析树,好的,所以最初我们的栈再次,栈是解析树的边界。

最初我们只有解析树的根,那是它自己的边界,它只是一个符号,我们还没有处理它,所以e在栈上,E在解析树中未展开,现在我们将使用生产式e去tx,所以我们将有t和x作为他的孩子,接下来会发生什么。

E从栈中弹出,T和x被推入栈中,现在注意解析树的边界是tx,那些是,这些是没有处理的叶子,要么是未匹配的输入,要么是未展开的非终结符,事实上,栈确切地记录了哪一个被留下,大多数,所以t在栈的顶部。

X在栈的下面,好的,嗯,我们仍然没有消耗任何输入,现在如果我们查看t int条目,它说使用t去int y,所以现在我们可以通过int y扩展t,现在会发生的是t从栈中弹出,Int和y被推入栈中。

现在注意栈是int y,X从顶部到底部,解析树的边界是int y x,现在我们有了一个情况,栈顶有一个终结符,所以现在我们将尝试将其与输入中的第一个终结符匹配,确实它们匹配,所以int被弹出栈,终端。

抱歉,输入指针在输入中前进,我在这里记录了,通过仅仅丢弃我们已经处理过的输入部分,所以现在我们有times一个int剩下要处理,int已经从栈中移除,所以现在栈顶是什么。

是y y确实是边界上未处理的最左边的东西,表格说对于非终结符y在输入times上,应使用生产式y到t乘t,所以让我们放在这里,现在会发生什么,为什么它会弹出栈,t乘t将被推入栈,现在注意栈是t乘t x。

前沿,解析树的未处理前沿是t乘t x,所以现在栈顶是终端,它与输入中的下一个终端匹配,所以弹出栈顶的终端,前进输入指针,现在t是我们的最左非终端,输入流中的下一个东西是int,表格说在这种情况下。

我们应该使用生产式t到int y,那意味着t被弹出栈,y被推入栈,注意栈是int y x,解析树的未处理前沿,从左到右是int y x,再次,栈顶是终端,它与输入流中的下一个终端匹配,它们匹配。

现在我们已经消耗了所有输入,美元符号是输入中剩下的唯一东西,但栈不是空的,好吧,那么这意味着什么,嗯,如果栈不是空的并且我们没有了输入,那么栈上剩下的所有东西最好生成空字符串。

所以从现在开始我们最好只使用epsilon生产式,确实,表格说当y是下一个非终端美元符号,我们处于输入的末尾,我们应该使用生产式,Y到epsilon,所以y到epsilon,这意味着y被弹出栈。

epsilon被推入栈,epsilon是空字符串,所以什么都没被推入栈,现在只剩下x,在x是下一个非终端美元符号是,我们处于输入的末尾,所以美元符号是我们的下一个符号,那么表格也说使用生产式。

X到epsilon,然后会发生什么,嗯,X被弹出栈,什么都没被推入,因为生产式是x到空字符串,现在我们看到美元符号在栈顶,美元符号在输入中,于是我们清空了栈,我们到达了输入的末尾。

因此我们接受这是一个成功的解析。

P28:p28 07-02-_First_Sets - 加加zero - BV1Mb42177J7

在接下来的视频中,我们将讨论如何构建LL(1)解析表,在这个特定视频中,我们将首先看看如何构建称为first sets的东西。

在我们深入本视频主题之前,即称为first sets的东西,我们需要稍微谈谈如何构建解析表,或构建解析表的条件是什么,因此我们感兴趣的是我们正在构建什么,我们正在构建一个解析表。

并且我们想了解对于给定的非终结符,A,这是最左非终结符和给定的输入符号,下一个输入符号t,我们想了解什么,在什么条件下我们会进行移动,A去α将替换非终结符A为右侧,α,这意味着表格中的a项将是α。

我们有两个情况想要这样做,好吧,所以第一个是如果α可以推导出t在第一个位置,这意味着从α开始,有一些推导,一些序列的移动可以是零或更多,移动将导致t出现在派生的字符串的第一个位置,如果有这样的推导。

那么在这个点上使用移动a去α将是好主意,因为然后我们可以匹配t,最终α可以生成t,然后我们可以匹配t,然后继续解析输入的其余部分,好吧,所以在这种情况下,当α可以生成t在第一个位置时。

我们说t是first of alpha的元素,t是α可以产生的东西之一,可能有更多的东西,但t至少是α可以产生在非常第一个位置的东西之一,一个终端,我应该说α可以产生在非常第一个位置的一个终端。

现在有另一种情况,一个稍微更复杂的情况,在这种情况下我们可能想要进行移动,或者我们想要进行移动,如果我们看到a是最左非终结符,并且t是下一个输入,那么我们想要替换a由a去α正确。

我们考虑的情况是如果α不能推导出t,所以,α不能在任何序列的移动中推导出t,实际上这意味着t不会在first of alpha中,α不能在任何序列的移动中推导出t。

实际上这意味着t不会在first of alpha中,好的,接下来是输入符号t,我们仍在考虑以a为最左非终结符的情况,现在下一个输入符号是t,这听起来不太乐观,因为我们有一个要匹配的输入符号t。

而我们接下来要做的推导,最左非终结符不能生成t,所以,但结果并非无望,实际上我们可能仍能解析这个字符串,即使情况允许,α也能到ε,若α能推导出ε,若α能完全消失,我们可以基本擦除α。

那么语法中其他部分可能匹配t,那么会在什么情况下呢,条件是,若a产生α,α可通过0或更多步到ε,α最终可完全消除,若t能在语法中紧接在a后,这必须有推导才合理,应有推导,我们使用a,a是推导的重要部分。

你知道,从开始符号,a后紧接着是期待的输入符号,在这种情况下,如果能去掉a,通过去epsilon,我们仍会按计划进行,可能语法其他部分会匹配,这种情况下我们测试什么,什么条件下能做好,我们希望能做到。

语法中t可跟在a后,我们说t在a的后续,a后可跟t等,这是易混淆的重要点,我想强调,注意不是推导t,a不产生t,t在衍生后出现,这里的a和t,与a产生的内容无关,这与,a在衍生中可出现的位置有关。

若t可在a的衍生后出现,则称t在a的后续中,本视频将聚焦于第一部分,下一视频将看后续集,再下一个视频将讨论如何整合,构建解析表。

好的,现在关注视频主题,计算First集合,所以这里,首先需要定义First集合,对于任意字符串a,实际上是x,这里是一个字符串,可以是单个终结符,可以是单个非终结符,或语法符号字符串,好的。

如果x通过某些步骤,在首位产生t,则t是,First集合中的终端,好的,所有可能的首位产生终端,将在First集合中,出于技术原因,稍后明确,也需要跟踪x是否产生epsilon。

尽管epsilon不是终端符号,如果x通过0或更多步产生epsilon,则说epsilon在First集合中,这被证明是需要的,我们需要跟踪x,是否产生epsilon,以计算给定语法符号的。

First集合中的所有终端,好的,这里是算法的概述,嗯,首先对于任何终结符,嗯,终端只能产生自身,所以这里的每个终端符号,应该说t是终端,对于每个终端符号,它的First集合仅包含该终端,好的。

现在考虑非终结符x,好的,所以这里是非终结符x,epsilon在First集合中的条件,如果有epsilon产生,如果x直接到epsilon,显然x可以产生epsilon。

epsilon应在First集合中,但如果x可以产生其他右侧,右侧所有内容,可以到epsilon,右半边可变为空,因此在这种情况下,空也在x的第一个,注意,这仅在,这些可以时发生。

仅当所有a都是非终结符本身时,才有可能发生,显然,如果右半边有任何终结符,则该产生式永远无法完全变为空,好的,我们至少会生产那个终端,但如果右半边的每个非终结符都能产生空。

意味着空在所有这些非终结符的第一个,并且右半边没有终结符,那么空将在x的第一个,还有另一种情况,这是我们利用跟踪空产生的地方,所以让我们假设我们有这样的产生式,好的,假设前n个符号。

这里a1到an都可以变为空,所以这都可以消失,好的,并被替换为空字符串,那么,这意味着,所以,如果我们有这样的推导,好的,然后通过一些移动,呃,它变为alpha,这意味着x可以通过,呃,一系列移动,呃。

推导出alpha本身,好的,所以i可以通过擦除所有a来变为alpha,我忘了在这里放alpha,结尾,在a_n后面应该有alpha,好的,这意味着什么,这意味着alpha中的任何东西,也将在x的第一个。

所以如果右半边的任何前缀可以消失,那么剩余的后缀,alpha,无论alpha是什么,都会被留下,那么alpha的第一个将是左半边非终结符的子集,在这种情况下是x,好的,好的,好的,这就是第一集的定义。

以及如何计算它们,好的,我们得计算终端,和非终端,好的,这就是这些,这些第二条规则涵盖非终端,正如我前面提到的,这对任何其他语法序列也是定义明确的,我的意思是,抱歉,语法中的任何其他字符串,不会。

如果我知道如何为终端计算它,并且我知道如何为非终端计算它,那么我也可以为语法中的任意字符串计算它。

好吧,所以现在让我们做一个例子,让我们看看这个语法,让我们看看我们是否能计算第一个集合,让我们从简单开始,做终结符,好吧,所以对于终结符,实际上,你知道,非常直接,加号是加号,乘号是乘号。

每个终结符都在集合中,第一组,每个终端的第一组只是,第二组包含该终端,余下的依此类推,这并不值得写出来,嗯,所以将是,嗯,open的第一组将是open,close的第一组将是discloper。

我想这就是所有我们需要做的,好的,行,这是终结符的第一组,现在看看更有趣的,谈谈第一个非终结符,那么,第一个v怎么样,看e的生产规则,记住我们的规则,我们知道t中的任何内容,也将是第一的e。

所以第一的t是v的子集,好的,所以为了知道第一的e是什么,我们必须知道第一的t是什么,至少至少要知道第一的b的一部分,我们必须知道第一的t,那么让我们继续计算第一的t,让我们试着现在得到那个集合。

第一的t实际上很容易,因为如果我们看t的生产式,我们可以看到它们在第一位产生终端符号,所以第一的t的唯一可能性和可能性是n和int,由于t只有两个生产式,并且它们都在非常第一位有一个终端符号。

没有其他终端符号可以通过t在第一位产生,所以我们可以直接从语法中读取第一的t,它是n和int,好的,现在让我们回到思考第一的e,所以记住我们还有另一个情况需要跟踪,或者我们抱歉我们需要考虑,所以可能是。

所以显然第一的t中的一切都在第一的e中,我们已经记下了这一点,但如果t可以转到epsilon,那么第一的x中的东西也可能在第一的e中,但现在我们计算了第一的t,我们看到epsilon不在那里。

第一的t总是生成至少一个终端符号,所以永远不会有一个情况x可以贡献到第一的v,因为t总是保证生成至少一个终端,所以实际上我们写在这里的子集根本不是子集,它是相等,第一的t和第一的b相等。

所以第一的e也是n和int,所以现在让我们看看第一的x,好的,所以第一的x显然加号在第一的x中,因为x的一个生产式立即在第一位置产生一个加号,所以我们可以直接将加号添加到第一的x中。

然后x有一个epsilon生产式,所以它也可以转到epsilon,这意味着epsilon也在第一的x中,那么关于第一的y,第一的y,它与生产式的结构相似,对于,加号我们看到我们有一个生产式。

在这里有一个终端在第一位置,这就是乘法,所以第一个y含有乘法,然后y还有一个epsilon产生式,为什么我能直接到epsilon,所以epsilon也在y的所有首次集中,实际上这就是这个语法的全部。

这些都是语法中所有符号的完整首次集,终结符只包含它们自己在首次集中,然后我们计算的非终结符有这些集合,因此我们结束了首次集的讨论。

P29:p29 07-03-_Follow_Sets - 加加zero - BV1Mb42177J7

本视频将讨论,构建解析表的构造,通过查看如何构建跟随集。

这是跟随集x的定义,回忆一下,给定符号的跟随集,实际上并不关于,该符号可以生成什么,而取决于该符号可以在哪里出现,该符号在语法中的使用位置,我们说t在x的跟随集中,如果语法中存在某个位置。

某个推导中终端t可以紧跟在符号x之后,好的,因此对于所有这样的t,它们构成了x的跟随集,这是如何计算跟随集的直观理解,假设x可以生成两个符号a和b,显然b可以生成的第一个位置,也在a的跟随集中。

因此如果x生成ab,然后通过更多步骤可以得到,a生成b生成t贝塔,那么t紧跟在a之后,显然b的first集在a的跟随集中,因此基本规则是,如果在语法中,两个符号相邻。

第二个符号的first集在第一个符号的跟随集中,好的,现在另一个有趣的事实是,如果符号在产生式的末尾,让我们看看这里的b,我声称,左边的跟随集中的任何东西,都会在b的跟随集中,在这种情况下。

x的跟随集是b的跟随集的子集,让我们看看这一点,假设我们从起始符号开始,好的,最终得到x后跟t,好的,x和t周围可能还有其他东西,但让我们暂时忽略这一点,我们只关注xt,然后我们可以使用这个产生式。

x生成ab,一步后可以得到abt,现在我们知道t在x的跟随集中,并且t也在b的跟随集中,好的,x的跟随集中的任何东西也将在b的跟随集中,我们可以将这一观察推广到生产式末尾的情况,因此,生产结束时发生。

是它的后续集,将包含此符号左侧生产的后续集,生产结束是什么,如果b可以到epsilon,如果b可以消失,那么a将出现在生产结束时,好的,因此,如果b可以到epsilon,那么x的后续集也将是a的后续集。

继续这里,在我们的例子中,我们从这里开始,我们从开始符号开始,我们得到了xt,一步我们得到了a b t,因此t在b的后续集中,但现在b可以到epsilon,因此我们也可以得到a t。

因此t也在a的后续集中,最后有一个特殊情况,记住我们有特殊符号标记输入的结束,那可以跟随什么,输入的结束在开始符号的后续集中,这又是另一种方式,跟踪我们将在输入用尽时做什么。

我们将在构建解析表时看到它是如何使用的,但我们总是添加,嗯,作为初始条件,美元符号在开始符号的后续集中。

现在让我们看一下计算后续集的算法的草图,正如我们刚才所说,嗯,美元符号在开始符号的后续集中,现在我们看看每个生产,好的,a去alpha x beta,我们专注于这里x,好的,如果我们看看每个生产。

并且我们看看生产右侧的每个符号,好的,beta的第一个,好的,在这个生产中可以跟随x的,第一个将在x的后续集中,并注意我们只是减去epsilon,如果在beta的第一个中。

我们不再对后续集中的epsilon感兴趣,epsilon从不出现在后续集中,因此后续集总是终端集的集合,现在算法的第二部分是,如果我们有一些生产beta的后缀可以到epsilon。

因此epsilon是beta的第一个,好的,因此,生产的后缀可以完全消失,然后,如上一页所述,左端符号的跟随将在x的跟随中。

这就是计算跟随集的规则,所以现在让我们通过一个例子,这是我们的语法再次,我们将计算语法中每个符号的跟随集,所以让我们从开始符号开始,我们将从跟随开始,E,根据定义我们知道美元符号在e的跟随中。

我们很容易得到这一点,现在的问题是b的跟随中还能有什么,好吧,所以为了弄清楚这一点,我们不得不看看语法中在哪里使用它,好吧,所以记住,跟随集是关于符号的使用,而不是它产生的内容,好吧。

这是e被使用的地方,我们可以看到它仅被一个终端符号跟随,所以当然右括号在e的跟随中,对吧,e还有另一个使用的地方,它在这里,它出现在一个生产的右端,那么然后我们知道任何在x的跟随中的东西。

也将要在e的跟随中出现,这是一个约束,我肯定会把它写在这里,好的,这只是跟随集之间关系的属性,当我们完成计算它们时,这并没有立即告诉我们任何新的东西,在e的跟随中。

但我们知道随着我们前进并了解x的跟随中的东西,我们不得不将它们添加到e的跟随中,让我只是在这里划分一下幻灯片,所以我们将我们知道的,关于跟随集之间关系的属性放在左手上,我们将实际的跟随集放在右手上。

好的,所以现在这就是仅有的两个地方,这就是仅有的两个地方,e在语法中被使用,为了进一步取得进展,我们需要知道一些关于x的跟随的信息,好的,如果我们将在e的跟随上取得进一步进展。

我们需要弄清楚x的跟随中有什么,所以让我们专注于这一点一分钟,语法中x在哪用?仅在一个地方用,就是这里,好的,它出现在生产式右端,因此左边的符号是x的后续集的子集,所以。

我们知道e的后续集是x后续集的子集,好吧,这意味着什么?所以x的后续集是e后续集的子集,b的后续集是x后续集的子集,这意味着这两个集合相等,x的后续集和e的后续集,无论它们最终是什么。

都必须是相同的集合,我们已经看了e在语法中所有使用的地方,我们已经看了x在语法中所有使用的地方,我们无法了解更多关于,e和x后续集中的内容,我们不必向任何集合添加其他内容,所以我们完成了。

所以我们可以关闭这个集合,我们知道e的后续集包括美元符号和闭合n,我们也知道x有相同的。集合,相同的后续集,好吧,现在让我们继续讨论t的后续集,好的,t的后续集中会有什么?

我们再次需要看t在语法中的使用,t用在两个地方,第一个在这里,第一个生产式,那么t的后续集中会有什么?可能是x的first集中的任何东西,好的,因为x紧跟在t之后,如果我们回顾之前的视频。

x的first集中只有两件事,一个是加号,所以这个加号肯定在t的后续集中,让我们回顾一下,抱歉,它是如何发生的?我们可以从e到tx,好的,现在使用第一个生产式,我们看到x在t之后。

然后在一步中我们可以到t加e,现在我们有了一个推导,其中加号跟在t之后,这就是为什么加号在t的后续集中,好吧,x的first集中另一个东西是epsilon,因为这里x有ε产生式,但记住我们并不关心。

我们不包括ε在后续集中,因此x没有贡献其他,嗯,到,呃,到t的后续集中,但由于x可以到ε,记得那意味着什么,这意味着从这里回头看t的第一次使用,这个x可以消失。

这意味着e的后续集中的任何内容也在t的后续集中,现在我们知道e的后续集,所以我们可以添加那些东西,好的,让我在这里写下来,以免我们忘记,所以e的后续集是t的后续集的子集,好的,我们不会再需要这个事实。

但写下它是有用的,也许,现在我们已经完成了对x的使用,我们已经在t的后续集中包含了由这个产生式暗示的,所有我们可以包含的内容,所以现在我们要看看t的另一个使用的地方,那就是这里,好的。

所以在这里我们看到t在产生式的右边,所以y的后续集中的任何内容也可以在t的后续集中,所以y的后续集将是t的后续集的子集,所以现在我们可以去处理y的后续集,为了确定t的后续集将会是什么,我们不得不。

我们需要知道y的后续集,那么y在语法中在哪里使用,嗯,只有一个地方,那就是这里,并且注意y出现在产生式的右边,这意味着左侧符号的后续集将被包含在y的后续集中,所以t的后续集将是y的后续集的子集。

现在再次我们有两个后续集是彼此的子集,y的后续集是t的后续集的子集,t的后续集是y的后续集的子集,我们知道这两个集合必须相等,好的,所以我们可以在这里写下y的后续集包括加号,美元符号和闭括号。

就像t的后续集,现在我们已经完成了,我们,我们呃,关于t的后续集和y的后续集,我们已经遵循了所有关于如何t的后续集,将事物纳入,可包含在t跟随中的内容,我们已经算出,我们查看语法中所有y被使用的位置。

并根据上下文添加所有可能的内容,我们没有被迫添加更多内容,因此我们完成了,好的,我们可以关闭这些集合,它们已经完成,所以现在,我们完成了e的跟随,X t和y,我们已经处理了所有终结符号,但抱歉。

所有非终结符号,但我们仍然需要计算终端符号的跟随集,与first集的情况不同,终端符号的跟随集实际上可能很有趣,所以让我们看看open paren的跟随,在推导中open后面可以跟什么。

Well open friend仅在一个地方使用,在这里,好的,因此open print后面可以跟的是e的first中的内容,并记住e的first与t的first相同,因为t总是在第一个位置产生内容。

而t的first是什么,它是open for an,它,好的,如果你仔细想想,嗯,这完全有意义,在任何有效,任何有效字符串中,在这个语法中,open print后面可以跟什么。

它要么是一个嵌套的括号表达式,要么是一个整数,特别是你不能在open后面立即有8次或a,也不能在open后面立即有输入结束,你不能在open后面有输入,Stop并有一个有效字符串。

所以现在让我们看看close paren的跟随。

好的,那个集合里有什么,再次,我们查看符号被使用的地方,它仅在这里使用,因为它出现在生产式的右端,右括号,接下来是什么,那是加美元和闭合,好的,现在让我们继续,看看操作符,让我们看看加号的后续。

所以加号在哪里使用,那仅用于这里,所以e的第一个元素将出现在加号的后续中,我们已经知道e的第一个元素是什么,那是一个左括号和一个整数,好的,记住e不能变为epsilon,所以e永远不能完全消失。

这是因为t总是产生至少一个终端,因此,只有e的第一个元素在加号的后续中,不是因为他从不因为,因为e不能变为epsilon,我们只需要包括e的第一个元素在加号的后续中,好吧,再次,如果你考虑一下一分钟。

这完全有意义,加号后面能有什么,嗯,它可以是一个整数,好的,这应该是,它可以是加法的第二个参数,或者它可以是另一个嵌套表达式的开始,它不能是乘法,它肯定不能是输入的结束。

因为你总是需要在加号后有另一个参数,呃,呃,我想那就是它,我想这是所有其他可能性,好的,好吧,现在让我们看看乘号的后续,我们可以在乘号后面,乘号在这里使用,所以t的第一个元素将出现在乘号的后续中再次。

好吧,我认为我们已经知道什么,那就是与e的第一个元素相同,那是打开,再次,这完全有意义,乘号后面能有什么,它要么是另一个嵌套表达式的开始,要么是一个整数,它肯定不是一个加号或输入的结束,好的,再次。

t不能变为epsilon,所以那是唯一的东西,这些是乘号的后续中唯一可能的东西,然后我们只剩一个符号,要看整数的跟随,好的,它在语法中的哪里使用,嗯,就在这里,没错,所以接下来会发生什么。

它将包括第一个y中的所有内容,第一个y中有什么,乘法在第一个y中,epsilon也在第一个y中,但记住,我们不包括epsilon在跟随集中,所以y贡献乘法到跟随事件,但现在因为y可以到epsilon。

有一个epsilon产生式为y,这意味着这个int实际上可以,最终成为这个生产的右端,好的,嗯,它可以结束,y可以消失,然后t可以跟随的任何内容也可以跟随结束,对。

所以我们必须包括t的跟随中的内容在it的跟随中,t的跟随中有什么,那是一个加号,那是一个美元,那是一个关闭,好的,那告诉我们什么,那告诉我们几乎任何东西都可以跟随一个int,但作为打开不能跟随结束。

所以你不能在int之后立即有另一个嵌套表达式,没有中间的运算符,对吧,这完成了跟随集的计算,对于这个例子。

P3:p03 01-03-_The_Economy_of_Pr - 加加zero - BV1Mb42177J7

你好,在这视频中,我们将讨论我称为编程语言经济的话题。

这个视频的想法是,在我们深入了解语言实现之前,呃或设计,我想谈谈语言在现实世界中的运作方式,以及为什么某些语言被使用,而其他语言不被使用,如果你环顾四周,实际上会出现一些明显的问题。

任何思考编程语言超过几分钟的人都会想到,一个问题是有这么多这些东西的原因是什么,我们有数百,如果不是数千种日常使用的编程语言,为什么所有这些都需要存在,为什么一种编程语言,例如,就足够了相关问题。

但略有不同是为什么会有新的编程语言,考虑到我们已经有了这么多编程语言,呃,为什么需要创建新的,最后,我们如何知道当我们看到一个好的编程语言时,什么使一个好的编程语言,什么使一个坏的编程语言。

我只想用这视频谈谈这三个问题,正如我们将看到的,我认为这些问题的答案很大程度上独立于语言设计和实现的细节,但本身非常有趣。

所以让我们从为什么有这么多编程语言的问题开始,至少对这个问题的部分答案并不难找到,如果你思考几分钟,你会意识到编程的应用领域有非常独特和冲突的需求,即很难设计一种语言。

实际上能在所有情况下为所有程序员做所有事情,让我们举一些例子,你可能不太会想到的一个领域,嗯是科学计算,这些都是为工程应用主要进行的重大计算,但也包括大型科学和长期运行的实验,模拟实验。

那么这些计算的需求是什么,通常你需要很好的浮点支持,缩写为fp,你需要很好的数组和支持数组操作,好的,因为在大多数科学应用中,最常见的数据类型是大型浮点数组,你也需要并行性,好的,好的,嗯。

今天要达到足够性能,你真的需要利用这些并行性,在这些应用中,并非每种语言都很好地支持所有这些,这实际上不是你需要的东西的完整列表,但这是一些需要区分的东西。

但一种传统上在这些方面做得非常好的语言是FORTRAN,FORTRAN在科学界仍然被广泛使用,它最初是为科学应用而设计的,若记得,名即公式翻译,随时间演变,已不太像原始语言,但始终保留科学计算核心。

仍为该领域领先语言,现完全不同领域:商务应用,那需要什么?这里需要如,嗯,别想丢失数据,企业为获取数据费尽心思,他们需要保存数据的方法,你知道,他们希望这极其可靠,你需要良好的报告功能,好的。

因为通常你想对数据做点什么,因此,你需要良好的报告生成设施,还有,嗯,你想利用数据,数据实际存在于许多现代企业,最宝贵的资产之一,因此你需要良好的设施来询问你的数据,我们称之为数据分析,再次。

这不是你需要的东西的详尽列表,但它是,嗯,代表性的,我会说,最常用的语言之一,这类应用是SQL,数据库查询语言,关系数据库及其编程语言,应该说,但最著名的是SQL,在这个领域占据主导,另一个,嗯,领域。

再做一次系统编程,因此,我的意思是,如嵌入式系统,控制设备的东东,操作系统,诸如此类,这些有什么特点,我们需要,需要对资源有极低层控制,系统编程的要点是管理好资源,因此我们真正需要对资源有细粒度控制。

并且常常有时间方面,所以你可能有实时限制,需要能考虑时间,因为这些是设备,它们需要在一定时间内反应,比如网络设备,需要对网络做出响应,很多很多,很多很多关于时间重要的例子,这只是两个方面,我有点。

我的意思是,这里空间不够了,所以就此打住,但再次这些代表了系统编程中需要的东西,今天可能最广泛使用的系统,编程语言或C语言家族,以及一定程度上的C++家族,和,你可以看到这些,不同领域的要求完全不同。

在一个领域重要的东西,或在一个领域最重要的东西与另一个领域不同,很容易,我认为可以想象,至少很难将所有这些整合到一个系统中,能做好所有这些事情。

这引出了我们的第二个问题,为什么会有新的编程语言,好的,现在有这么多语言存在,我们为什么还需要设计一个新的,我将从与问题无关的观察开始回答这个问题,所以让我花点时间解释一下。

我声称程序员培训是编程语言的主要成本,我认为这真的很重要,所以只是强调一下这里重要的部分,就强调一下重要的部分,是程序员培训,教授程序员语言的成本,所以如果你考虑一种编程语言,为了使这种语言被使用。

必须发生几件事,必须有人设计它,但这并不真的很贵,那只是一或很少几个人,通常有人需要构建编译器,但这也并不真正花费很多,对于一个大型的编译器项目,可能十到二十人,我可以构建相当不错的编译器。

真正的成本在于所有用户和教育他们,所以如果你有成千上万的用户使用一种语言,教授他们所有语言所需的时间和金钱是主要成本,我并不是说,仅仅是购买教科书和上课等实际美元支出,也是程序员决定。

学习这种语言是否值得,你知道许多程序员自学,但那是他们时间的使用,以及他们时间的成本是真正的经济成本,所以如果你考虑教授,一百万程序员的人口,一门语言,这实际上是一项相当重要的经济投资,从这个观察。

我们可以很容易地做出几个预测,再次,这些只是这些只是预测,现在遵循这个声明,如果你相信这是真的,所以让我擦掉并修复它,所以首先,呃,预测是广泛使用的语言,将缓慢变化,为什么应该是真的呢?

如果我改变一种被许多人使用的语言,我必须教育那个社区中的每个人关于这个变化,因此,即使是相对较小的语言扩展,语法的小变化,小的新功能,甚至只是编译器接口的简单变化,如果你有很多用户。

教他们所有关于这个需要很长时间并且非常昂贵,所以随着这些语言变得广泛使用,它们的变化率,它们的变化率将减慢,并且这预测随着时间的推移,随着编程世界的增长,随着世界上越来越多的程序员。

我们预计最流行的语言,将拥有更大和更大的用户群,所以拥有更大和更大程序员基础的将变得越来越僵化,进化越来越慢,实际上实践中看到的非常一致,现在另一端,这一观察几乎做出了相反预测,容易开始。

容易开始新语言,事实上,启动新语言成本很低,为什么?因为从零用户开始,所以开始几乎零训练成本,即使只有几个用户,教他们的成本,语言变化并不大,并不很高,新语言能更快进化,能更快适应变化情况。

实验新语言几乎不花钱,这两者间有张力,好,嗯,当,程序员何时选择,你知道,广泛使用但可能变化不快的现有语言,和全新语言,他们会选择如果生产力,如果他们的生产力,现在超过培训成本,如果他们认为。

花一点时间和金钱学新语言,短期内将更高效,他们就会转换,好,你知道,所以,何时可能发生,专业,你知道,嗯,综合所有这些,嗯,你知道,语言最可能被采用,填补空白,好再次。

这是从程序员培训为主要成本的事实得出的预测,我指什么?我指编程语言存在目的,人们使用它们,嗯来完成工作,因为我们仍处于信息革命中,不断有新的应用领域出现,因此出现了新的编程类型,每隔几年甚至更频繁。

所以只是,你知道,从近期历史来看,你知道移动应用现在相对较新,并且正在建立许多新技术来支持移动计算,几年前,互联网本身是一个新的编程平台,像Java这样的许多新编程语言正是在那时开始的。

新的编程领域开放,因为技术变化,所以人们想用软件做的事情也变了,这为语言创造了新的机会,旧语言变化缓慢,所以它们很难,适应,你知道,适应这些新领域,它们并不真正适合它们,因为我们之前讨论过的。

在之前的幻灯片和问题中,因为很难有一种语言包含您想要的所有功能,所以有,嗯,所以新的语言并不一定适合这些应用领域,它们适应新情况的速度很慢,这往往促使新的语言出现,所以当有一个新的机会和一些应用领域。

如果有足够的程序员支持语言,通常会出现一种新语言,我只想指出另一个可以做出的预测,嗯,从这个单一观察中,嗯,那就是程序员培训,再次强调这是编程语言的主要成本,那就是新语言,往往看起来像旧语言。

也就是说新语言很少,如果曾经是完全新的,它们与某些,一些前辈语言有家族相似性,有时是许多前辈语言,为什么会这样呢,部分原因是很难想到真正的新事物,但我也认为这有一个经济利益,即它减少了培训成本。

让新语言像旧语言,利用人们对旧语言的了解,让人更容易学新语言,让人更快地学,最经典的例子是Java和C++,Java被设计得像C++,我认为,有意识地让现有C++程序员,开始学Java。

最后,我们可以问什么是好语言,不幸的是情况不太清楚,我只想说没有普遍接受的指标,强调没有普遍接受的指标,这意味着人们不同意好语言的标准,有很多指标,人们提出了很多衡量语言的方法,但大多数人我不认为。

这些是很好的衡量标准,当然没有共识,看看程序员的世界,他们不能同意最好的语言是什么,要说服你们,看看,任何新闻组帖子,人们进行半宗教争论,为什么一组语言或特定语言比另一种语言更好,即使在研究社区。

在科学界,在语言设计者中,我认为没有普遍接受的共识,为了说明试图制定这样的指标的困难,让我讨论一下人们认真提出的,一个指标,一个好的语言是人们使用的,我在这个问题上打问号,我不相信这个说法。

经过片刻反思,我可以说服你这不是一个好指标,从正面来看,这个论点是,这是一个非常明确的指标,它测量语言的人气,所以有多少人实际上在使用它,可能更广泛使用的语言有更好的原因,某种程度上。

也许它们是更好的语言,在反面,这个论点的论据是,这是一个非常明确的指标,它测量语言的人气,所以有多少人实际上在使用它,可能更广泛使用的语言有更好的原因,某种程度上,也许它们是更好的语言,但这意味着。

如果你相信并遵循其逻辑结论,嗯,Visual Basic是最好的语言,优于所有其他编程语言,我对Visual Basic并无偏见,它是一个,它是一个精心设计的系统。

但我不认为设计师是Visual Basic,会声称它实际上是世界上最好的编程语言,正如我们在刚刚的讨论中看到的,除了技术卓越之外,还有许多其他因素,影响一个编程语言是否广泛使用,事实上。

技术卓越可能甚至不是语言可能被使用的,最重要的原因,它更多地与是否解决了一个,利基或应用领域有关,其中没有更好的工具,一旦它建立并拥有大量用户,当然,历史惯性有助于其生存。

这就是为什么我们仍然有Fortran和COBOL等,你知道,来自很久以前的语言,我们今天可以,如果我们现在重新开始,设计得更好,因此,结束关于编程语言经济的这段视频。

我认为要记住的两件最重要的事情是,应用领域有冲突的需求,因此,嗯,这很难,设计一个系统,嗯,包含你想要的一切,所以你不能拥有所有,你想要的功能在一个单一的系统中,一个连贯的设计,至少很难做到这一点,嗯。

所以向现有系统添加新功能需要很长时间,第二点是程序员培训是编程语言的主要成本,这两件事,这两个观察,这些真正解释了为什么我们得到新的编程语言,因为旧语言很难改变,当我们有新的机会时。

往往更容易、更直接地设计一个新的语言,而不是试图移动整个程序员社区和现有系统,来适应这些新的应用,而不是试图移动整个程序员社区和现有系统,来适应这些新的应用,而不是试图移动整个程序员社区和现有系统。

P30:p30 07-04-_LL1_Parsing_Tabl - 加加zero - BV1Mb42177J7

本视频将整合,我们学过的关于,一阶跟随集构造LL(1)解析表。

目标是构造文法G的解析表T,通过产生式完成,我们将逐个进行,并依次考虑文法G中每个产生式a->α,第一种情况是,我们试图确定是否可以使用a->α,且T恰好是α的第一个。

如果已知某个终结符T是右边的第一个,那么如果a是非终结符的最左边,且T是下一个输入标记,通过a->α扩展将是好棋,因为α可能通过更多产生式匹配T,因此,我们将α添加到解析表的a,T项,右边的α就对了。

我们感兴趣的其他情况是,如果我们需要消除a,好的,所以如果a不可能匹配T,比如说T不是α的第一个,或者我们有其他情况想要消除a,那么使用产生式,a->α,前提是α实际上可以到ε,所以α可以完全消失。

我们可以消除所有a的痕迹,且T遵循a在文法中,所以T能够跟在抱歉,T能够跟在a和一些派生之后,所以如果T在a的跟随集中,且产生式的右边代码到ε,那么添加移动,当a是非终结符的最左边且T是下一个输入时。

我们可以通过a->α扩展a,最后,对于美元的特殊情况,因为美元实际上不是一个终结符号,如果我们到了输入的末尾,好的,所以当我们有a,还有一些东西留在栈上,特别是我们还有非终结符。

a仍然是我们的最左非终结符,但我们用完了输入,那么我们唯一的希望是彻底消除a,所以我们想选择一个a的产生式,它可以到ε,所以我们寻找一个产生式,a->α,其中ε在α的第一个中,美元可以在a的派生中跟随。

这就是过程,构造解析表的规则,现在让我们看个例子,这是我们一直在看的语法,现在看看,解析表会是什么样子,解析表将包含,以语法中的终结符命名的列,所以这里会有,左括号,右括号,左括号n,加乘,和int。

行将由非终结符命名,所以会有e t,X和y,现在我们将逐个应用规则,看看解析表中,我们创建了什么条目,所以什么时候使用e->tx,首先要注意的是,这个产生式右边,不能产生epsilon。

所以tx总是产生至少一个终结符,我们感兴趣的第二个情况是,这个产生式是否能产生零,因为它可能产生epsilon,抱歉不适用,我们只需要考虑它能产生什么,在第一个位置,所以只有t的first,中的东西。

即,左括号n和int,有两种情况会使用e->tx,即如果e是,最左非终结符,下一个输入是左括号n,另一个是下一个输入是int,现在看看这个产生式,什么时候使用t->(e)?如果t是最左非终结符。

这是左边的符号,并且下一个输入是(n),因为那是右边唯一的东西,那么扩展为(e)将是好主意,只有一种情况使用那个产生式,对于另一个t产生式,我们将在t是最左非终结符时使用,没有其他情况。

所以对于另一个t产生式,输入中有整型,所以在这里,y中会有,这里漏了一列美元,所以最后把美元放在这里,现在已涵盖前3个产生式,看看这个产生式,何时使用x到+e,显然右端第一个是+,左端终结符是x。

x+项我们想扩展为,x到+e,类似地对于y,第一个涉及y的生产式,当y是终结非终结符,我们尝试扩展,输入中有乘,将使用生产式,y到*t,好的,现在只剩两个空产生式,这些是实际上能到空产生式的。

那么何时使用,何时使用x到空或y到空,回忆我们需要知道x的follow,以知道何时使用,呃,x到空,所以我们在上次课计算了,但让我们再写一次,好的,那么x的follow是什么,嗯。

我们需要看x在语法中的使用,x在这里使用,它出现在产生式的右端,所以会是e的follow中的东西,e的follow是什么,因为e是开始符号,所以美元在e的follow中,右括号在b的follow中,对。

那么y的follow是什么,另一个我们需要知道follow集的地方,我们需要看y在哪里使用,所以y在这里使用,这意味着t的follow中的所有都在y的follow中,y的follow将包括,呃。

x的第一个,因为x可以跟在t后,所以加号将在y之后,但x可以变为epsilon,因此e之后的一切都在t之后,因此也在y之后,所以y跟随的其他两个是美元符号和闭括号,好的,所以这表明,如果我们处于,嗯。

当我们有一个x,好的,让我们,让我们,让我们专注于x变为epsilon的生产,假设我们有一个x在栈上,好的,栈顶和美元符号是下一个输入,嗯,我们能做什么,我们已到输入的末尾,我们必须去掉x。

显然我们想使用x变为epsilon的移动,好的,这有道理,另一个情况是,这个跟随集告诉我们使用x变为epsilon,如果有闭括号在栈上,因为x不能自己产生闭括号,但希望栈上的其他符号能产生它。

一旦我们消除了x,好的,所以在这种情况下我们也应该使用x变为epsilon,然后类似地对于y跟随的y变为epsilon的生产,跟随中有三个东西,三个终端在y的跟随中,我们应该使用y变为epsilon。

如果它们是下一个输入,所以如果我们看到一个加号,我们试图扩展一个y,我们将使用y变为epsilon,如果我们看到一个闭括号,我们看到它,并且我们试图扩展到y,我们使用y变为epsilon,最后。

如果我们完全没有了输入,我们仍然有一个y剩余,我们将使用y变为epsilon,这就是完整的解析表,好的,现在你可以看到这将在每种情况下工作,好的,对于我们的最左非终结符,和每一种可能的输入或缺乏输入。

我们有一个我们可以使用的生产,现在这张表中有许多空白条目,那些对应什么,假设我们试图扩展x,下一个输入符号是一个开括号,这里没有条目,好的,那是一个错误,这是一个解析错误,每当遇到表格中的空白项时。

当你尝试使用空白项时,当你解析时,这将产生一个解析错误,因为这告诉我们,存在空白项的事实,它告诉我们没有有效移动,没有方法解析该字符串,我们发现了这一点,在我们尝试访问表格中的错误或空白项时。

现在让我们考虑尝试构建一个L1解析表时会发生什么,对于不是L1的语法,让我们看一下之前看过的简单,左递归语法,所以S去S a是一个生产,S去b是另一个生产,为了构建这个表格的部分。

我们需要知道第一和跟随集,所以让我们看一下S的第一个好吧,所以S可以在第一个位置产生什么,显然它可以产生一个b,没有epsilon,没有,没有可能从S生成epsilon,所以,实际上。

那将是S的第一个中唯一的东西,那么S的跟随是什么,我们可以跟随一个S,它是开始符号,所以显然,美元符号在S的跟随中,然后终端,终端a在第一个生产后的S右侧出现,所以a也在S的跟随中。

现在我们可以构建我们的表格了,它将会是一个非常小的表格,我们只有一个非终结符,然后有两个终结符,a和b以及输入结束符号,所以这张表格中只有三个条目,潜在地好吧。

所以现在让我们逐个生产看看我们应该放在哪里,所以让我们先看一下第二个生产,因为那没什么特别的原因,所以如果S去b,我们什么时候应该使用它呢 显然,如果我们看到一个b在输入中,这将是一个很好的选择。

因为因为因为右边的第一个包括b好吧,s到b用于输入b,现在s到sa呢,不能生成epsilon,仅关注首位置能产生什么,s首为b,sb项也有移动,s到sa移动,现在看到问题,这里有个多移动项,多重定义项。

好的,这意味着什么,如果输入s,要扩展s,好的,尝试做,如果栈顶非终结符是s,下一个输入符号是b,好的,嗯,表没明确移动,不是确定的,可能移动有两个,这样知道,语法不是1型因为,如果建1型表。

表中有多个移动,某个位置,某个项抱歉,他们误用项,如果你,让我再说,如果你想要,如果你建表,表中有项多于一个移动,没有唯一移动给解析器,语法不是1型。

刚说,如果表任何项多重定义,语法不是1型,这是1型语法的定义,确保语法是1型唯一方法,机械检查语法是1型方法,建1型表看所有项是否唯一,我们知道,然而,某些类语法保证不是1型,不是1型,哪些不是1型。

好的,任何左递归的语法都不是L1,好的,任何有歧义的语法也保证不是L1,但这并不是一个详尽的列表,其他语法也不是L1,因此特别地,如果一个语法需要超过一个标记的向前看,它就不是L1。

但即使那不是完整的列表,甚至那些超出这个范围的语法也不会是L1,所以这些加起来就是你可以做的快速检查,来测试一个语法是否保证不是L1,但如果一个语法是左因子的并且不是左递归的,并且是无歧义的。

那并不保证它是L1,唯一确定方法就是构建解析表,并查看它所有条目是否唯一,不幸的是,大多数编程语言,它们的上下文无关文法,或描述它们的语法,大多数编程语言不是L1,L1语法。

太弱无法捕获常用编程语言中所有有趣和重要的结构,有更强大的形式化描述语法或实用语法的形式,我们将在未来的视频中查看这些,事实证明它们建立在我们在过去几周中学到的一切之上,对于L1语法。

所以那些都不会浪费,但它们以更复杂的方式组装这些想法来构建更强大的解析器。

P31:p31 07-05-_Bottom-Up_Parsin - 加加zero - BV1Mb42177J7

这是一系列关于自底向上解析的视频中的第一个。

解析。

首先要知道的是,自底向上解析比确定型解析更通用,自顶向下解析,回忆一下我们讨论过的递归下降,这是一个完全通用的解析算法,但需要回溯,现在我们专注于确定型技术,上次我们讨论了L1或预测性解析。

现在我们要换挡,讨论自底向上解析,结果是,即使自底向上解析更通用,它同样高效,它使用了我们在自顶向下解析中学到的所有想法,实际上,自底向上解析是大多数解析器生成工具首选的解析方法。

自底向上解析器的一个优点是它们不需要左因子化文法,我们可以回到我们示例的自然文法,自然在这里是带引号的,因为我们仍然需要编码加号和乘号的优先级,自底向上解析器不会处理歧义文法,让我们举个例。

考虑一个自底向上解析器如何处理以下典型输入字符串。

首先要知道的是,自底向上解析通过逆向产生式,将字符串还原为开始符号,通过运行逆向产生式,所以这里有一个例子,左侧是字符串的状态序列,右侧是使用的产生式,需要注意的是,让我们只看第一步。

我们开始时是整个字符串,我们开始时是终端符号的字符串,我们挑选了一些终端符号,在这种情况下,只是这一个特定的英寸,我们运行了一个逆向产生式,我们用产生式的左侧替换了这里的int,我们开始时。

我们匹配了产生式的右侧int,并用左侧替换了它,所以int在这里逆向变成了t,然后在下一步中,我们取了int times t,这个我们正在处理的字符串的子串,并用该产生式的左侧替换了它。

int times t被替换成t,等等,在每一步中,我们都在匹配字符串的一部分,我在每一步中划下被替换的部分,我们运行,这匹配了某些产生式的右侧,然后我们用左侧替换了那个子串,最后。

整个字符串被替换成e,我们最终到达了开始符号,所以我们从一个输入字符串开始,所以这是一个输入字符串,这是我们的输入字符串,我们的标记输入字符串,我们以起始符号结束,如果你按这个方向阅读移动。

如果你从底部开始向上阅读,嗯,这些都是产生式,实际上,这整个东西是一个推导,这只是从底部到顶部的正常推导,但在这个方向,当我们倒着运行时,从字符串开始到起始符号,我们称这些为规约。

我还没有确切告诉你我们如何决定进行哪些规约。

你可能会想,我是如何知道要执行这个特定序列的规约的,这是自底向上解析的另一个有趣属性,所以如果你按这些产生式的反向阅读,它们追踪一个最右推导,所以从这里开始与e。

所以我们要记住解析器实际上正在这个方向上运行,所以这是解析的方向,但现在我们要看解析器反向采取的步骤,我们将看到它实际上是一个最右推导,所以这里e去到了t加e,e是唯一的非终结符,但然后e这里是扩展的。

它是最右边的非终结符,然后t被扩展,它也是最右边的非终结符以得到int,现在t是最右边的非终结符被扩展以得到in times t,然后这是唯一的和最右边的非终结符。

所以我们最终得到了整个输入字符串和times int加int,这引出了关于自底向上解析的第一个重要事实。

即自底向上解析器反向追踪一个最右推导,所以如果你在自底向上解析上遇到麻烦,回到这个基本事实总是有帮助的,自底向上解析器追踪一个最右推导,但它以相反的方式通过使用规约而不是产生式来做。

所以这里是再次显示的规约系列,这是由那些规约构建的解析树,嗯,我认为这是一个非常有帮助的图片,如果我们动画它,以看到序列的步骤和自底向上解析器真正在做的事情,所以从这里我们开始输入字符串。

我们在这里显示相同的输入字符串,现在我们将只走过自底向上解析器采取的一系列步骤,一系列的规约,展示如何构建完整解析树,基本思想是每一步执行归约,记得做归约时,用某些产生式的左部替换右部子节点。

就像我们做自顶向下解析时,我们会使右部,成为左部的子节点,这里也一样,这是输入中的子节点,然后使其成为父节点,现在可以看到接下来会发生什么,自顶向下解析器从开始符号开始。

通过扩展前缘的非终结符逐步构建树,当前,在部分构建的解析树的当前叶节点,自底向上解析器将从最终解析树的所有叶节点开始,整个输入,并在其上构建小树,它将粘贴在一起。

所有迄今为止组合在一起的子树以构建完整树,再走几步看看如何发生,所以在下一步,嗯,我们从int times t到t,因此int times,和以另一个t为根的子树成为这个非终结符t的子节点。

你可以看到我们已将这三个子树,粘贴在一起成为一个更大的树,随着解析的进行,原始输入的越来越大一部分将被粘贴成越来越大的树,下一个归约,嗯,输入末端的int,和嗯,归约为t,然后归约为e,然后在最后。

剩余的三个子树都将粘贴成一个完整的解析树,以开始符号为根,通过组合小解析树构建解析树,它自下而上而不是从开始符号扩展,自顶向下,它从树的叶节点向上构建到根。