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

175 阅读1小时+

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

P32:p32 07-06-_Shift - 加加zero - BV1Mb42177J7

本视频中,我们将继续讨论自底向上解析,使用所有自底向上解析器的主要策略。

所谓的移入-归约解析,这是上次我们学到的最重要内容的快速回顾,这一特定事实有一个重要后果。

所以让我们思考一下移入-归约解析的状态,其中我们有一些字符串alpha,等,贝塔和欧米伽,假设下次归约将贝塔替换为x,好的,记住我们正在逆向运行产生式,那么我声称欧米伽必须是终结符串,为什么是这样呢。

如果你考虑一下,那么当x被替换时,我们取这个,如果我们看前向步骤是逆向步骤,所以记住解析器这样运行,用x替换beta,则x必须是右最非终结符,意味着x右边没有非终结符,因此所有字符,所有标记。

或字符串中的任何东西都是终结符,结果是,右最非终结符右边的终结符,正是自底向上解析器实现中未检查的输入,如果有alpha,X,Omega和我是,X是最后一个非终结符,这是未读的输入,这是未检查的输入。

标记我们在解析中的位置将是有用的,我们的输入焦点是,我们将使用垂直线来做这件事,所以我们将只是玩,在已读的左侧和实际工作的右侧画一条垂直线,我们正在处理这个,左侧为终结符和非终结符。

解析器已看到所有内容,右侧为解析器未看到的内容,我们不知道外面有什么,尽管我们知道都是终结符,竖线仅标记两个子字符串的分界线。

实现自底向上解析,实际上我们只需要两种操作,嗯,移位和归约操作,我们已经讨论过一些减少移动,因此我们引入了移位移动。

现在让我们这样做,因此,移位移动读取一个输入标记,我们可以解释这一点,或通过将垂直条向右移动一个标记来表示,因此,如果我们的输入焦点在这里,如果我们想读取更多的输入标记,那么我们只需将垂直条向右移动。

这表示现在解析器知道下一个终结符号,现在我们可以开始处理它,它可以对它做些什么,并与它匹配以执行再次减少的目的,垂直条右侧的内容。

解析器还没有看到,减少移动是在左字符串的右侧应用逆生产,因此,如果我们有一个生产,A 去 x y,我们在这里立即有 x 和 y 位于垂直条的左侧,因此,这是我们的焦点点,好的,x 和 y。

生产右侧的内容就在这里,那么我们可以做一次减少,我们可以用左侧替换右侧,这是一个减少移动。

这是上次视频中的示例,这恰好是仅显示减少移动的示例,现在也显示了垂直条,这显示了在每个减少执行时输入焦点的位置,当然,我们现在知道缺少的是,移位移动的序列,这里是移位移动和减少移动的序列。

将初始输入字符串带到开始符号,因此,让我们更详细地走过这个过程,因此,我们将逐步进行,我们将显示每个移位和每个减少移动,现在,除了我们下面的输入字符串,我们还有一个指针显示我们在,输入中的位置,因此。

我们还没有看到任何输入,我们的输入指针在整串的左侧,因此,第一步是做一个移位,然后我们再做另一个移位,然后我们再做另一个移位,现在,我只是看着之前的例子,如果你回头看那个例子。

你知道接下来我们需要做的是减少,并记住我们只能减少到箭头的左侧,因此,我们只能在箭头的这一侧减少,因此,我们总是必须在执行减少移动之前读取足够的输入,然后我们执行另一个减少移动,好的。

接下来要做的是移位操作,我们还没解释如何知道是移位还是归约,我们将会讲到,我只是展示存在一系列移位和归约操作成功解析,这个例子,现在我们把整个输入移到了这,抱歉我们已移过整个输入,没有更多输入可读。

现在只能做归约操作,幸运的是从这一点开始有一系列归约操作我们可以执行,这里我们归约int,然后我们归约t加t,哦忘了,我们首先归约t到e,然后我们归约t加e回到开始符号。

结果这个左串,垂直线左边的部分,可以用栈实现,因为我们只在垂直线左边立即做归约操作,所以它总是垂直线左边字符串的一个后缀,归约发生的地方,所以移位操作是将一个终结符推入栈中,读取一个输入标记并推入栈中。

然后归约弹出栈中的一些符号,那是产生式右部,然后它推入一个非终结符到栈中,那是产生式左部。

现在可能在一个给定状态中,移位或归约可能导致有效解析,特别是如果移位或归约是合法的,如果你能做其中一件事,那么我们说有一个移位,归约冲突,解析器可以读取一个输入标记并推入栈中,或者它可以执行一个归约。

嗯,如果可以通过两个不同的产生式归约,那么有一种称为归约,归约冲突,好的,所以归约,归约冲突总是坏的或几乎总是坏的,它们通常指示,语法中通常某种严重问题 Schiffre的冲突是不好的。

但它们通常更容易消除,所以如果你有归约归约冲突,特别是当你为cool构建语法时,那么你正在做非常严重的事情,如果你有移位,归约冲突,那么这几乎是可以预见的,嗯,你,你可能需要使用优先级声明,嗯。

以消除它们,我们将在另一个视频中讨论更多,但一般来说,如果你有这些冲突之一,这意味着在某些状态下,解析器不知道该做什么,你需要重写语法。

P33:p33 08-01-_Handles - 加加zero - BV1Mb42177J7

本视频中,将介绍自底向上解析中的另一个重要概念。

句柄的概念。

回顾自底向上解析使用两种动作,我们有移位动作,只读取一个输入标记,并将竖线向右移动一格,还有归约动作,将竖线左侧的生产右侧立即替换为左侧,即生产左侧,因此在这种情况下,生产必须是。

A 到 x,Y,并回顾上一视频的内容,左侧字符串可以通过栈实现,栈顶由竖线标记,移位将终端符号推入栈中,归约弹出栈中零个或多个符号,这将是某些生产的右侧,然后它将把一个非终结符推入栈中,即该生产的左侧。

自底向上解析中的关键问题,也是我们尚未解决的问题,是如何决定何时移位何时归约,让我们看看这个示例语法。

并思考解析的一步,我们已经将一个标记移入栈中,栈中有 int,然后 times 隐含在后面,我们还没有看到,此时我们可以决定通过 t 到 int 归约,因为我们有生产,T 到 int 在这里。

因此我们可能会进入这个潜在状态或这个特定状态,栈中有 t,然后输入看起来像这样,但你可以看到,这将是一个错误,语法中没有以 t times 开始的产生式,上面没有这样的产生式,看起来像 t times。

因此如果我们做出这个动作,我们会陷入困境,我们可以继续进行归约来在字符串中翻找,但我们永远无法回到开始符号,因为没有方法处理包含 t times 子串的子串。

因此这告诉我们,我们并不总是想要归约,即使栈顶有某个生产的右侧也要重复这一点,即使栈顶有某个生产的右侧,进行归约可能是一个错误,我们可能想等待并在其他地方进行归约,我们决定的方式是,我们只想在。

若结果仍可还原为起始符,让我们看看最右推导,所以从起始符开始,经过若干步到达某个状态,记住,这意味着经过任意步到达某个状态,X是最右非终结符,下一步是用某个产生式的右侧替换X,记住,自底向上解析。

解析器实际上朝这个方向走,好的,这是归约方向,我们讨论推导方向,产生式方向,因为那是谈论字符串如何推导的最简单方式,我们想从起始符开始,但但,但解析器实际上逆着这些箭头的方向走,无论如何。

如果这是最右推导,这意味着,是的,在这种情况下将β还原为X是可以的,我们可以用X替换β,因为这不是错误,我们仍然可以通过一系列动作回到起始符,你知道,通过做更多的归约。

柄形式化了对在哪里可以执行归约的直觉,柄就是一个归约,也允许进一步归约回到起始符,我们显然只想在柄处执行归约,如果在不是柄的地方执行归约,即使看起来像是右侧,或实际上可能是某个产生式的右侧。

那并不意味着它实际上是一个柄,我们可能,如果我们在那里归约,我们可能会卡住,所以到目前为止我们只说了什么是柄,我们定义了柄,我们还没有说如何找到柄,实际上我们如何找到柄。

将占用我们关于解析讨论的大部分剩余时间。

在这个点上,我们知道足够多的关于自底向上解析的事实,所以在移位归约中,解析柄只出现在栈的顶部,从不在里面,事实上,这就是使用栈的正当理由,因为焦点点左边的字符串,我们知道所有动作将立即在焦点点左边发生。

无需深入查看栈内,因此栈足够。

这是一个非正式证明,嗯,仅栈顶出现操作符,基于减少步数的归纳,栈初始为空,因此成立,你知道,唯一可能减少在栈顶。

若有epsilon移动,减少后最右非终结符在栈顶,减少后,我们有栈,然后是非终结符,然后是竖线,这是最右非终结符,因为是右推导,下一个操作符必须在右,下一个操作符,它必须包含可能包含这些。

但要么在当前焦点,要么在右边,因为不能对最右非终结符左侧减少,需要一系列移位到达下一个操作符,栈顶有非终结符,按定义是最右非终结符,下一个操作符必须在右边。

因此移位,移位减少解析操作符总在栈顶,操作符不在最右非终结符左侧,这是移位减少足够的原因,移位操作仅移动竖线右,我们不需要移动它,自顶向下解析基于识别操作符,如视频开头示例,栈顶有右部并不意味着。

它是操作符,我们需要更聪明地执行减少。

P34:p34 08-02-_Recognizing_Hand - 加加zero - BV1Mb42177J7

欢迎回到本视频,我们将讨论识别句柄的关键思想。

识别句柄有好消息和坏消息,坏消息是没有已知的有效算法能识别一般的句柄,因此对于任意语法,解析时我们没有快速找到句柄的方法,好消息是有猜测句柄的启发式方法,和,呃,对于一些上下文无关文法。

对于一些相当大的上下文无关文法类。

这些启发式方法总能正确识别句柄,我们可以用韦恩图来说明情况,如果我们从一个包含所有上下文无关文法的集合开始。

那么无二义上下文无关文法是这些文法的一个子集。

然后是一个更小的集合,称为lrk文法,这里只是提醒一下,L代表从左到右,扫描,K代表向前看的标记数,现在lrk文法是,呃,一类最一般的确定性。

呃,确定性的,呃,文法,我们知道,但这些并不是实践中真正使用的,大多数实用的自顶向下工具使用,称为lark文法,它们是lrk文法的一个子集,然后我们主要讨论的是这些的一个简化。

称为简单lr文法或slrk上下文无关文法,这些包含关系是严格的,即对于每个k,都有ar k文法但不是slr k文法,同样,对于每个k,都有lrk文法但不是lalr k文法。

正如我们已经说过的,检测句柄并不明显,那么解析器知道什么,好吧,它看到了每一步的栈,它知道它已经构建的栈,因此让我们看看我们能从栈中得到多少进展,只是考虑我们能从栈中得到的信息,所以这里是我们的定义。

我们将说alpha是一个可行前缀,如果有一些omega,使得alpha bar omega是一个配置,一个有效的移位配置,一个有效的移位配置,一个有效的移位配置,简化解析,注意这里的alpha,这是栈。

omega是剩余输入,这意味着解析器知道这部分,解析器知道alpha,它对omega了解不多,它可以向前看,它可以查看omega的小前缀,通常仅一个标记,但它肯定不全知道。

那么,可行的前缀意味着什么,可行的前缀是一个字符串,不会超过句柄的右端,我们称它为可行前缀的原因,是因为它是句柄的前缀,只要解析器栈上有可行的前缀,就没有检测到解析错误,实际上。

这个定义只是在给某件事命名,它并不是什么很深奥的东西,那个alpha bar omega是可行的,只是说我们还没遇到错误,这是移位归约解析的一种状态,还没说如何识别它,或其他类似情况,嗯。

只是说这些是移位归约解析的有效状态,移位归约解析。

定义在某种程度上有用,因为它引出了最后一个重要事实,自顶向下解析的第三个重要事实,那就是对于任何语法,可行前缀集是正则语言,这真是个惊人的事实,这需要一点时间证明,但这是自底向上解析的关键。

至少所有自底向上解析工具都基于这一事实,可行前缀集可由有限自动机识别。

所以,我们将展示如何计算这个自动机,接受可行前缀,嗯,首先需要一些新定义,第一个定义是项的概念,现在,项是一个仅在右侧有点的产生式,看个例子,以t -> (e)为例,我们要做的是。

在右侧所有可能的位置上放置一个点,将有一个项,点在最左侧,将有一个项,点在最右侧,然后,将有一些项,点在连续符号之间,这种情况下有4项生产,特殊情况是如何处理空产生式,对于空产生式,没有,右边没有符号。

我们只说有一个项x->。这些项你会看到,如果你,如果你查看帮助页面和文献,它们是lr零项。

现在我们可以讨论如何识别可行前缀,问题是栈中只有生产右侧的碎片,总的来说,大部分时间栈顶没有完整的右部,大部分时间栈顶只有右部的一部分,结果发现栈上的内容并非随机,它,它,呃,实际上具有非常特殊的结构。

在这些片段中总是右部产生式的前缀,即在任何成功的解析中,栈上的总是右部前缀,某些产生式或产生式的。

让我们看个例子,考虑输入open closed for,这是我们最爱的文法之一,现在这种配置,栈上有左括号,记住这是我们的栈,我们还有,呃,输入中的右括号,这实际上是一个状态或有效状态,归约解析。

你可以看到,open for n e是生产的前缀,T 去 open e 闭 for,在我们将剩余的闭 for 移至栈上后,然后,我们将有完整的右半部分准备归约,所以这是项出现的地方。

项 t 去 open 括 e 点 闭括 n,这描述了这种状况,它说,到目前为止,我们已经看到了 open for e 的这个生产,希望未来能看到完成,另一种思考方式是,此项记录,我们正在制作的事实。

目前我们已看到这么多,点左为已看内容,也是栈上的内容,点右为待看内容,在可能减少前需等待,可能看不到,记住解析器不知输入,在这种情况下,当然,这是下一个符号,所以我们可以在预览中看到,但你知道在此时。

解析器并不确定接下来会发生什么,你知道,如果这个点再往左一些,可能会有很多,更多符号需要处理才能进行归约,所以无论如何,记录左侧的,我们已经看到的,记录右侧的,表示我们在栈上等待看到的,才能进行归约。

现在我们可以讨论栈的结构,它不仅仅是符号的任意集合,实际上它具有这种非常特定的结构,所以栈实际上是一个右端前缀的栈,栈总是具有这种组织,其中有一堆前缀堆叠,字面上堆叠在栈上,将要发生的是,i前缀。

如果我们从这个前缀栈中选择一个前缀,那必须是某个生产式的前缀,某个生产式的右端,这意味着栈上的i前缀,最终将归约到该生产式的左端,所以最终归约到xi在这种情况下,然后xi必须是缺失后缀的一部分。

在栈下部的那个前缀,所以如果我查看上一个前缀,紧挨着下面的,栈上的前缀sui。

当我进行这个归约时,xi需要扩展该前缀,更接近于该特定生产式的完整右端,好的,特别是将会有某个生产式,其右端的一部分已经在栈上,所以i减1的前缀,xi将扩展该前缀,然后还会有一些东西。

可能我们正在等待看到,甚至在xi放置后递归地,栈上所有高于前缀k的前缀,最终都必须归约到前缀k缺失的右端部分,右端上的alpha k,我有这个图像,如果你有一个前缀栈,我们总是在处理栈顶的前缀。

所以他们将始终在这里工作在右端,移位和规约,但每次执行规约,必须立即扩展栈下部的前缀,当这些,当一堆前缀通过规约从栈中移除时,然后当我们开始处理栈中较低的前缀时,所以让我们用一个例子来说明这个想法。

所以这是另一个输入字符串,我们将使用相同的语法,你可以,如果你想看语法,可以倒带,但让我们考虑这个状态,我们有,Uh 在栈中为 n int 星,并且我们有 int 关闭和剩余在输入中,好的。

那么哪些项会记录,栈结构是什么,项如何记录它,好吧,让我们从这里开始底部,实际上我们从底部向上工作,所以我们在栈顶有 in 星,所以这是我们正在工作的右半部分,这将是该生产的前缀。

T 去 int 星 T,好的,所以这说我们正在看,你知道,我们已经看到了 in 星,我们现在权衡,我没有显示项,但我只是显示了这个最终将使用的生产,下面的一个在这里,栈上的前缀在,在开和闭之间。

在这个结束,这是一个有趣的情况,实际上是 epsilon,所以现在栈上没有任何东西,但一旦星规约到 T 好,然后那个 T 将规约到 e,当然目前那里根本没有 T,我们只看到了 epsilon。

我们在栈上没有看到该生产的前缀,然后对于最后一个生产,栈中最深的一个,我们目前看到了 in 开括号,我们并且我们正在处理这个生产,He 去 open her in e 关闭,所以当这个 e 产生时。

这将扩展这个右半部分,现在我们可以用 uh 栈的项来记录所有这一切,Uh t 指向开点,E e 指向点 t 和 t 指向星点 t 好,它只是记录了我们之前幻灯片上看到的内容,到目前为止。

我们看到了这个生产的开放端,我们还没有看到生产右侧的任何内容,到目前为止,我们看到了这个生产的 in star,注意一下,这些生产的左侧最终将,成为右侧的一部分,右侧的右侧,作为生产右侧的一部分。

我们正在堆栈中下面工作,因此,当我们将 in star t 减少到 t 时,这将扩展这个生产。

当它减少到 e 时,这将扩展这个生产,总结这个视频,我们可以更精确地说,我们如何识别可行的前缀,问题的关键将是识别一系列的部分,生产右侧,其中每个这些部分,右侧最终可以减少到其前驱缺失的后缀的一部分。

下次在下一个视频中,我们将实际给出实现这个想法的算法。

P35:p35 08-03-_Recognizing_Viab - 加加zero - BV1Mb42177J7

在这视频中,我们终于要讲到自底向上解析的技术亮点,所以在前几个视频的所有定义之后,我们现在实际上将能够给出识别,可行前缀的算法。

所以让我们直接深入算法,第一步,嗯,实际上只是一个技术点,并不,嗯,并不那么重要,但我们无论如何都要做,因为这使事情更简单,那就是添加一个虚拟生产,S' 到 S 到我们的兴趣语法。

G 所以这里只是设置舞台,我们正在尝试计算 g 的可行前缀,我们正在尝试设计一个算法,以识别 g 的可行前缀,如果 s 是开始符号,我们只需创建一个新开始符号,S'为起始符,S'为新起始符。

S'仅有一条产生式,S'指向S,没错,这使我们确切知道起始符的使用位置,特别是,新起始符,S'仅在一个地方使用,在这条产生式的左侧,这使事情稍微简单些,回忆我们尝试做的事。

我们声称给定文法的可行前缀集是正则的,因此我们要做的是,我们将构造一个非,确定性有限自动机以识别可行前缀,好的,该nfa的状态将是文法的项,nfa的输入是栈,因此nfa读取栈,好的,然后它。

那么我们就标示这个,NFA将栈作为参数,它将说'是',那是可行的前缀或'否',它将从栈底到栈顶读取栈,它将从栈底开始,并读取到栈顶,我们的目标是编写一个,非确定有限自动机,识别解析器的有效栈。

所以这就是如何,我们知道解析器未报错,因为我们构造的自动机会始终输出,是,栈没问题,意味着它可以解析输入,或知道当前栈的内容,不像是任何有效栈,好的,让我们思考,嗯,我们,我们需要这台机器的动作是什么。

假设我们在状态,E箭头alpha点x beta,那是什么意思,所以到目前为止我们在栈上看到了alpha,好的,记住机器从栈底到栈顶读取,这记录了机器已经看到alpha在栈上的事实。

接下来在栈上看到什么是可以的,如果这个是一个有效的栈,如果在这一点上有alpha在栈上是有效的,那么当然如果下一个在栈上是x,那就好,所以我们有一个这样的转换,如果我们处于这个状态,正在处理这个生产。

并且在栈上看到了alpha,如果下一个输入是x的x,那么我们可以进入这个状态,现在记录我们看到alpha x在栈上,我们正在等待看到该生产的剩余部分beta,好的,这是非确定性有限自动机可以做的。

我们添加这种类型的移动,对于语法中的每个项目,如果点不在最右边,那么将有一个这样的移动,点移动到,对于点右侧出现的任何符号,另一类转换是,嗯,是这些,这些是更有趣的,所以让我们说我们处于这个配置。

我们再次看到了alpha,然后下一个在栈上是x,这里x是非终结符,我应该在之前的情况下说,嗯,x是终端或非终结符,所以这个x是任何语法符号,不仅仅是非终结符,但这里的数字4,这些动作专门针对非终结符。

好的,那么无论如何,如果x不在栈上,好的,假设我们已经看到了alpha,然后栈上的下一个不是x,那么有可能存在一个解析器的有效配置,我们看到了alpha,但随后x没有出现,答案是肯定的。

因为如我们之前所说,栈是部分右边的序列,所以栈上现在可能只有所有这些,此为alpha版本,栈顶下一个可能减税的未必是x本身,可能是最终可简化为x的某物,那意味着什么,意味着栈上的任何内容都必须源自x。

必须是可由x产生序列生成的东西,因为它最终将简化为x,所以对于每个像这样和每个x的生产,现在我们将添加以下移动,我们将说如果栈上没有x,然后可做ε移位,可直接移至尝试识别,从x派生出的右部。

仅这两种移动,要么,项目抱歉,要么,栈上的语法符号是我们寻找的,扩展右部前缀,此规则扩展前缀,说是,栈上看到更多,或尝试猜测前缀结束,如果α包含栈上的生产,这必须是x,必须这一点,这里标记另一个开始。

期待看到x派生。

两条新规则,每个状态都是接受状态,这意味着,如果自动机成功消耗整个栈,栈中的栈是可行的,注意不是每个状态都有每个可能符号的转换,因此会有很多可能的栈被拒绝,仅仅因为自动机卡住了,最后。

这个自动机的开始状态是项,S' 到 S,记住机器的状态是语法的项,我们添加了,这个虚拟产生式只是为了方便命名开始状态,现在让我们考虑我们的一种语法,我们一直在使用很多,所以这是语法。

现在我们将用额外生产来增强它,当p趋于e时,让我们看看这个自动机,识别该语法可行前缀的,这就是它,如你所见,它相当大,它有大量的状态和转换,我只想在这里展示给你,在我们描述如何计算它之前,就。

所以你有概念,这些识别语法可行前缀的自动机,实际上相当复杂,但现在让我们分解并看看它是如何产生的,所以让我们从这个机器的起始状态开始,所以我们有s' 到 。e,记住这说的是,我们希望能够还原到开始符号。

到我们的新开始符号,我们正在读取栈,我们希望看到栈上的e,但如果我们没有,那么看到从e派生的东西也会很高兴,从这个状态我们可以做出哪些转换,嗯,一种可能性是,我们确实,实际上在栈上看到了e。

在这种情况下,点简单地移动,说,是的,我们已经阅读了栈上的第一项,或者我们已经在栈上阅读了e,我们已经看到了这个生产的完整右侧,那将表明我们可能已经完成了解析,这是你会达到的状态。

如果你已经阅读了整个输入并成功解析了它,你已经还原了旧的开始符号,并准备还原到增强的新的开始符号,但如果你没有像看到e在栈上那样幸运,那么你需要希望你会看到从e派生的东西,有几个可能性,有一种可能性是。

我们可以看到一些最终会使用这个生产e到t的东西,因为我们还没有看到任何它,我们把点放在最左边,表示我们希望看到一个t,它可能然后还原e,它可能然后还原到s' 现在,如果我们没有在栈上看到单独的t。

另一种可能性是,我们可能正在处理这个生产e到t加e,同样我们还没有看到任何它,点在左手边,注意现在,我们关键使用非确定自动机的力量,所以这里我们不知道哪个产生式,将出现在哪个产生式的右侧栈上。

事实上我注意到这些产生式甚至不是 um,甚至不是左因子化的,所以我们不知道它将是仅仅一个 t 那里,或一个 t 加 e,但我们只是使用非确定自动机的猜测能力,让它选择哪一个来使用,记住确定性自动机接受。

如果任何可能的选择接受,所以它总能猜正确,直观上它将能够选择正确的现在,当然我们可以编译这个到确定性机器,Um,那将不必做任何猜测,但在这个级别我们编写非确定性机器。

非常有用不必弄清楚使用哪个这两个产生式,我们可以尝试两个并看看会发生什么,现在让我们关注这个状态 e 去点 t,有哪些可能性,一种可能性是我们看到一个 t 在栈上,然后我们已经看到了一个完整的右侧。

并注意当点完全在右侧时,那将指示我们准备做或减少,我们稍后会谈论这一点,但基本上这就是我们将如何识别句柄,当我们最终达到一个状态时点完全在右侧,那将说 ah,这可能是你想要减少的一个句柄。

现在如果我们不在栈上看到一个 t,那么我们需要看到从 t 派生的一些东西并且有几个可能性,一些可能性,一种可能性是它将是这个产生式 t 去 int 所以,因为我们只是再次开始这个产生式。

我们只是把点完全放在左边,另一种可能性是正在处理 t 去开 e 闭括号,和第三种可能性是我们在处理 t,去 int 乘 t,并且在每种情况下这里注意它都完全到左边,指示我们只是刚开始。

我们实际上还没有看到任何的右侧,还,现在让我们将焦点转移到该产生式 e 去点 t 加 e,这个项抱歉,Uh,一种可能性是我们看到一个 e 在栈上,我看到一个 t 在栈上,好的,那么点就移到。

另一种可能是我们看到来自t的,那么我们将转到以t开头的状态之一,注意这里我们已经有了自动机中的所有三个项目,我们只是去我们从项目中到达的状态,Egoes到dot t。

所以这项egoes到dot t加上e也可以移动到那三个状态,现在让我们关注这个,呃,这里t去dot open e close in well,这里只有一个可能移动,所以这只是一个终结符,它不是。

非终结符,所以不会有任何来自open的可能性,我们只需要看到open朋友和输入,所以这里只有一个可能转换,即我们看到open,抱歉,在栈上,现在,从这种状态再次,点在a旁边或就在非终结符左边。

所以这里我们可能看到栈上的非终结符,或者我们可能看到来自该非终结符的,如果我们看到栈上的非turtle,那么点就移过,我们得到t open e dot close for。

表示我们在栈上看到了open打印和e,我们仍在等待看到close括号,但我们也可能看到来自me的,好的,所以我们有这些两个转换到两个开始,呃,生产为你所有现在,让我们关注这个状态。

T去open pare dot close again,因为它是一个终端,点旁边的只有一种可能移动,我们必须看到那个open,如果我们看到任何东西,我们最终会得到项目。

T去open print e close parendot,现在我们识别了该生产右侧栈上的整个,让我们看看这个项,所以我们在这里,因为终端符号,唯一的可能性是读取栈上的终端符号。

所以这将是下一个项目e goes to t plus dot e,再次关注那个项目,你知道我们可能看到栈上的e,在这种情况下,我们将识别该生产的整个右侧,我们将有egos到t plus e dot。

或者我们可以看到来自me的,好的,那么我们将回到那两个状态之一,现在还有哪些产生式或项目需要处理,t 去 dot int,因此我们得在栈上看到下一个,那将是该产生式的完整右部。

下面 t 去 dot int 仍然存在。

乘以 t 再次,这里有一个终结符 int,因此这是我们需要在栈上看到的下一个东西,以保持该产生式的可行性,一旦我们看到int,嗯,我们想看到时间,最终处于这种状态,现在我们有了t旁边的点,再次。

一种可能是我们在栈上看到t,然后我们看到了这个产生式的完整右侧,但可能只看到从t派生的东西,t可能不在那里,但它可能处于我们仍在等待t出现的状态,通过一些归约序列,但我们需要看到t的派生,这种情况下。

我们转向3种状态之一,嗯,开始生成tea,这就是完整自动机,这就是所有状态,所有自动机的转换,识别该语法可行前缀的自动机。

P36:p36 08-04-_Valid_Items - 加加zero - BV1Mb42177J7

本视频中,我们将使用示例自动机,识别有效前缀,引入一个新概念。

有效项的概念。

为了唤醒你的记忆,这是上次我们停下的地方,这是完整的非确定自动机,用于识别示例语法的有效前缀,并使用标准子集构造阶段,我们可以构建一个等效于非确定自动机的确定自动机,确定自动机。

这是识别完全相同语言的确定自动机,这个自动机,这个确定自动机识别示例语法的有效前缀,但现在注意,每个状态都是一个项集,所以这些状态中有非确定自动机的状态集。

并回忆这意味着非确定自动机可能处于这些状态中的任何一个,特别是这个状态是起始状态,呃,因为它有项s' 箭头 dot e,呃,这个确定自动机的状态被称为各种,规范项集或规范lr零项集,如果你看《龙书》。

它给出了构建lr零项的另一种方法,而不是我给出的,我的方法有些简化,但我也认为更容易理解,如果你是第一次看到,现在我们需要另一个定义。

呃,我们将说,给定项对于有效前缀alpha beta,呃,是有效的,如果以下为真,从起始符号开始,这是我们的额外起始符号,然后一步x可以到beta gamma,这表示在解析alpha和beta之后。

在看到alpha和beta在栈上之后,有效项是可能栈顶的项集,我们可能可以,该项可能是非确定自动机的终止状态。

更简单地解释同一个想法是,对于给定的有效前缀alpha,对于该前缀有效的项,正是DFA在读取该前缀后处于最终状态的项,这些是描述在看到栈alpha之后的状态的项。

一个项通常对许多,许多前缀有效,E闭合对所有开括号序列有效。

只需查看自动机即可确认,并确认若看到开括号且这是起始状态,若看到开括号结束,我们进行此转换,最终到达此状态,然后每个开括号,我们看到只需在此状态中循环,因此若输入序列为5个开括号,则会有1、2,3。

4、5,所有循环在此状态,注意此项在此状态中,它是该状态项之一,仅表示此项对任何前缀或抱歉,任何开括号序列有效。

P37:p37 08-05-_SLR_Parsing - 加加zero - BV1Mb42177J7

本视频中,我们将真正实现自底向上解析算法,特别是我们将讨论SLR或简单LR解析,它将建立在有效项的概念上,以及我们在最近视频中讨论的可行前缀。

首先我们将定义一个非常弱的自底向上解析算法,称为LR零解析,基本思想是假设栈包含内容alpha,下一个输入是标记t,DFA,这是识别输入alpha的可行前缀的DFA,即当它读取栈内容时。

它将终止于某个状态s,并且只有两种情况,该解析算法需要处理,如果s是DFA的最终状态,包含项x->beta。那么,这是什么意思,这意味着,我们在栈顶看到了x->beta的完整右侧,并且进一步说。

所有内容都在栈下,仍然表示x->beta。是一个有效的项目,抱歉,是一个有效的项目,这意味着可以由x->beta减少,因此,如果我们看到一个完整的生产。在DFA的最终状态下的右侧。

那么我们只需通过该生产减少,另一种可能的动作是移位,如果我们最终处于一个状态i,x->beta。t,然后其他一些内容是有效的项目,这意味着在这一点上添加一个t到栈将是合适的,如果t是我们的输入,那么。

我们应该做一个移位,移动,嗯,LR零解析何时会遇到麻烦?可能有两个问题,它可能无法在两个可能的减少动作之间做出决定。

因此,如果DFA的任何状态有两个可能的减少,意味着它看到了两个完整的生产,并且可以由任何一个减少,那么就没有足够的信息来决定执行哪个减少,并且部分将不是确定性的,这称为减少减少冲突,所以再次。

这发生在特定状态有两个单独的项指示两个单独的减少时,另一种可能性是DFA在读取栈内容后的最终状态,可能有一个项表示减少,另一个项表示移位,这称为移位减少冲突,因此,在这种情况下,只有在状态中才有冲突。

另一个可能性是DFA在读取栈内容后的最终状态,可能有一个项表示减少,另一个项表示移位,这称为移位减少冲突,因此,在这种情况下,只有在状态中才有冲突,输入的下一个是?但在那种情况下。

我们不知道是否要堆栈t。

还是通过x去β减少,让我们看看dfa,识别可行前缀,我们最近几期视频一直在使用,实际上,这个特殊的dfa确实有一些冲突,让我们看看这个状态,我们可以通过egos到t减少,如果我们处于这个状态。

或者如果下一个输入是加号,我们可以做一次移位,所以在这种特殊情况下,如果下一个输入是加号,我们可以移位并使用这个项,或者我们可以减少并使用那个项,这个特殊的状态有一个移位,减少冲突。

这不是这个语法中唯一的冲突,呃,在这个语法中,不过,呃,在这个状态这里,我们有一个非常相似的问题,这里,如果下一个输入是乘号,或者我们可以通过t去int减少,所以这个状态也有一个移位,减少冲突。

改进lr零解析并不难,我们将在本视频中展示一种这样的改进,称为slr或简单lr解析,这将通过,通过添加一些启发式来改进我们何时移位何时减少,这样冲突的状态就会更少。

将lr零解析修改为slr解析的改动实际上非常小,我们只是在减少情况下添加了一个新的条件,所以之前如果我们看到了x去β点在dfa的最终状态,回忆一下那意味着什么,这意味着β在栈顶,它是可行的。

所以减少是可行的,现在,我们有一点更多的信息,所以注意这个自动机并没有利用,输入中接下来会发生什么,这个决定完全基于,栈的内容,但减少可能无意义,基于下一个输入符号,我们如何充分利用它,若你想。

会发生什么,我们有栈内容,以β结束,现在我们要移动,用x替换它,若下一个输入符号是t,记住这里有个竖线,后跟t,这意味着什么,这意味着x在推导中须在t前,换句话说,T将遵循x,若T不能遵循x。

若T不能是跟在非终结符x后的终结符,那么进行此规约无意义,因此我们仅进行规约,若T在x的跟随集中,我们仅添加该限制,这是解析算法唯一的改变。

因此在这些规则下若有冲突,要么移位,归约或连续归约,则语法不是SLR文法,注意这些规则相当于检测句柄的启发式,我们考虑两个信息,栈的内容,这是DFA为我们做的,它告诉我们栈顶可能有哪些项。

以及输入中接下来是什么,我们可以用它来细化归约决策,对于没有冲突的文法,意味着每个状态,在这些规则下,每个可能的状态都有独特动作,那么这种启发式方法是精确的,你知道,对于那些语法。

我们只是定义那些语法为SLR语法。

让我们考虑运行示例中发生的变化,确定性的自动机,用于识别语法可行前缀,我们已经看了几段视频了,回忆一下我们曾有移位,在两个状态下,按lr零规则减少冲突,先看状态,上层状态,这里将移位,输入中有加号。

该项指示我们做什么,它告诉我们有加号,正确动作是移位,现在问题是何时减少,但我们只会在。减少,如果输入是e,e后是什么,我们很久前就算过了,但提醒你,这里的e是文法的原始开始符号。

所以$符号最终会出现在e后,follow v的另一种可能是闭合n,因为在文法的这个点上,闭合n紧跟在e后,只有这两种可能,这意味着在这个特定状态下我们将归约,如果输入结束,或者下一个。

输入中的下一个标记是闭合的n,将移位,如果输入中的下一个标记是加号,在其他任何情况下,我们将报告解析错误,因此不再有任何移位,减少此状态下的冲突,对于任何可能的输入,总是有一个唯一的移动,对于其他状态。

情况也类似地得到了改善,所以这里我们将移位,如果输入中有乘号,我们将减少输入,如果符合t,t的跟随是什么,我们回忆一下,嗯,我们很久以前又计算过,我碰巧知道,嗯,那是什么,所以我会直接告诉你。

它包括了t跟随的所有内容,E,美元符号和闭合符,但随后是加号,因为语法中此处使用+,t后的只有这些,仅在无输入时缩减,或下一个输入是闭合或加号,也没有移位,不再移位缩减,解决此状态冲突,因此。

此语法是SLR语法,许多语法不是SLR,强调SLR是LR零的改进,但它仍然不是一个非常通用的语法类,所有歧义语法,例如,不是SLR,但我们可以在SLR情况下稍作改进。

我们可以通过使用优先级声明来改进SR解析,以告诉它如何解决冲突。

让我们回到最自然,也是最模糊的整数加法和乘法语法,我们之前看过这个语法,如果你为这个语法构建dfa,如果你遍历构建这个语法的可行前缀的dfa,你会发现有一个状态包含以下两个项,一项说如果我们看到e乘e。

我们已经在栈上看到了e乘e,现在我们可以通过e去e乘e来简化,另一项会说如果输入中有加号,我们应该移位,注意这正好是乘法比加法优先级更高的疑问,当你处于这种情况时,你应该简化,从而将两个e组合在一起。

首先组合乘法操作,还是应该移位加号,在这种情况下你将处理第一个,因为它在栈的顶部,所以在这种情况下,乘法比加法优先级高的声明解决了简化的冲突,所以我们不会移位,最终不会有移位,简化冲突。

注意优先声明这个术语实际上相当误导,这些声明并不定义优先级,它们并没有直接做到这一点,它们真正定义的是冲突解决,它们说做这项移动而不是那项,碰巧在这种特殊情况下,因为我们处理的是一个自然语法。

一个简单的加法和乘法语法,冲突解决恰好产生了我们想要的强制优先级的效果,但在更复杂的语法中,各个语法部件之间有更多的交互,这些声明可能不会像你期望的那样强制执行优先级,幸运的是你可以打印出自动机。

工具通常提供一种方式让你检查解析自动机,然后你可以确切地看到冲突是如何解决的,以及这些是否是你预期的解决方案,我建议当你构建解析器时,特别是如果它是一个相当复杂的解析器,你应该检查解析自动机。

以确保它按你期望的方式工作。

所以现在我们可以给出slr解析的算法,m是我们的自动机,识别可行前缀的解析自动机,初始配置将是垂直线,全部靠左,所以栈是空的,这是我们的全部输入,我们在末尾附加美元符号表示输入的结束。

现在我们将重复直到配置仅包含开始符号在栈上,一美元输入,意味着所有输入消失,将整个输入缩减为起始符号,因此中间配置将写作αω,其中α是栈的内容,ω是剩余的输入,我们要做的是运行,M运行机器在当前栈α上。

如果m拒绝α,如果m说α不是一个可行的前缀,我们将报告解析错误,我们现在就停止,如果m接受alpha且在状态,如果结束于含i项的状态,然后看下一个输入,称为a,我们将做什么,如果有项在I中。

说看到终端a没问题,好的,这就是我们的移位操作,然后我们会减少,如果有减项在有效项集中,下一个输入可遵循左侧非终结符,这些就是我们之前讨论的规则,然后报告解析错误,如果这些都不适用,好的。

这个算法有趣之处在于,如果你仔细阅读,并思考一段时间,你会意识到这一步其实不需要,我们不需要在这里检查,因为m是否接受栈并不重要,因为在这里报告解析错误的步骤,如果这些步骤都不适用。

这已经意味着我们永远不会形成无效栈,即所有栈都将始终有效,解析错误将在这一行被捕获,并且,符号不可能构成前缀,实际上,此错误检查不需要,M始终接受栈。

现在,最后一步如有冲突,意味着在某些状态下,对于某些输入符号,不清楚是移位还是归约,则语法不是SLR k,k又是前瞻量,实践中,我们只使用一个标记的前瞻,因此,通常只查看输入流中的下一个标记。

P38:p38 08-06-_SLR_Parsing_Exam - 加加zero - BV1Mb42177J7

欢迎回到本视频,我们将做一次扩展的SLR解析示例。

回顾一下,这是语法解析自动机,我们在前几期视频中一直在看的,这是非确定性自动机的确定性版本,我们上次构建的,我已经把所有状态都编了号,让我们看看解析输入int时会发生什么,乘以int,回顾一下。

我们在末尾添加了美元符号,以指示输入结束的位置,这只是输入结束的标记,因为这是解析的开始,我们还没有看到任何输入,因此竖线位于输入的最左侧,机器从状态一开始,栈上没有任何东西,竖线再次位于输入的最左侧。

所以栈为空,所以它在状态一终止,这些是解析器初始状态中有效的可能项,所以在这项中,我们看到,有两个告诉我们在这个状态下移入整数是可以的,当然第一个输入是整数,所以没有归约动作。

这里面的其他项也都有它们的点,都在项的最左边,当前状态无减步可能,唯一可能操作是移位,整数移位是可以的,总结一下,嗯,在解析器的初始配置中,dfa在状态一停止,它甚至从未离开状态一,从那里开始,结束。

未读任何输入,因为栈为空,状态指示我们行动:移位,于是我们处于以下状态,栈上有整数,输入中有乘法,那种情况会发生什么,嗯,自动机会再次读取栈,从栈底开始,我们在起始状态,然后我们读取一个整数。

栈上有整型,我们进入此状态,此状态告诉我们能做什么,它告诉我们一种可能是归约t到整型,但再次我们只会,如果后续输入是t和times的序列,哪个是后续输入项不在t的后续,所以times不在t的后续。

因此在此处归约不是可能的选择,只剩下另一项考虑,这里我们看到这项说我们可以移入一个times,所以如果times,输入中的下一个东西,就是它,移入是可以的,所以DFA在状态三保持。

因为输入中有times,呃,移动是移入,这让我们进入此配置,栈上有整型和times,times在栈顶,整型在它下面,输入中有整型,现在又发生了什么,DFA将读取整个栈,所以从栈底开始。

它首先看到的是一个整型,它移动到那个状态,然后它看到一个times,所以它移动到这个状态,现在在这个特定状态,有哪些可能性,首先我们可以看到没有归约移动,没有项目点的右边是全部,所以唯一的可能性是移入。

我们可以移入如果后续输入是a开头的,这没有更多用处,我们可以移入如果后续输入是整型,这正是我们看到的,所以DFA在状态十一终止,在那个状态下的移动是移入,这让我们进入此状态,栈上有整型times整型。

输入已完,我们到了输入的末尾,让我们看看栈上的整型times整型会发生什么,自动机读取整型times整型,最终回到状态三,状态三,告诉我们如果下一个输入项是times,我们可以移入,但它不是。

或者我们可以归约如果输入中的任何东西是t的后续,在输入中是,实际上,美元跟在t之后,因此在输入的末尾,t可以跟在栈上,这意味着减少t到int是可行的,所以一旦我们做了,一旦我们做了减少,T变成int。

我们最终处于状态,Int乘t,这是栈的内容,当然我们仍然在输入的末尾,因此,dfa将再次读取整个栈内容,从底部到顶部,首先,它读取栈底部的int,然后它看到乘号,最后,它读取栈顶部的t。

最终处于一个新状态,状态四,关于这一步有趣的是,dfa通过状态图走了不同的路径,而不是上次,这是因为栈的内容改变了,我们不仅仅是在栈上添加东西,我们没有延长之前的路径,实际上。

我们用新的符号替换了栈上的某些符号或符号,在这种情况下,非终结符t,导致dfa走了不同的路径,现在,状态四中的这一项告诉我们该做什么,它说,如果输入的后续内容在t的跟随集中,我们可以通过t到n乘t减少。

再一次,美元在t的跟随集中,我们将做那个减少,现在我们只剩下栈内容t。

当然我们仍然在输入的末尾,让我们看看现在会发生什么,所以现在当然栈的内容发生了更根本的变化,因此,dfa只是朝着完全不同的方向前进,它读取t,最终处于这个状态,这个状态说,我们可以推入一个加号。

如果有加号在输入中,再次,没有更多的输入,或者我们可以通过e到t减少,如果美元,即输入的结束,在e的跟随集中,它是,我们将做的减少将是那个,现在我们只有栈内容e。

让我们看看在这种情况下会发生什么,现在我们转移到状态二,我们只有一项,S'到e点,这是一个减少移动,美元紧随S',因为那是开始符号,因为那是开始符号,此时接受,一旦到达该项即为归约动作。

P39:p39 08-07-_SLR_Improvements - 加加zero - BV1Mb42177J7

本视频中,我们将结束关于SLR解析的讨论,我们将给出完整的SLR解析算法,并讨论一些重要改进。

上期视频中讨论的SLR解析算法有一个主要低效,那就是当自动机,当它读取栈时,实际上大部分工作是多余的,要看到这一点,请考虑栈,因此我们有我们的栈,这里是底部,这是栈顶,每一步都在做什么。

可能会将东西推入栈中,可能会添加一个符号,可能会弹出一些符号并推入一个符号,但基本上,栈顶的符号数量很少会改变,每一步,但大部分栈保持不变,然后重新运行自动机在整栈上,因此,所有工作都重复。

一切与上次堆栈相同,重复工作,然后在堆栈顶部做一点新工作,显然,如果可以避免,我们可以让算法运行得更快。

利用自动机大部分工作重复的观察,在每个步骤中,只需记住自动机在每个堆栈前缀上的状态,我们将改变堆栈的表示,我们将改变堆栈中放入的内容,之前,我们只在堆栈上有符号,但现在我们将成对。

栈中的每个元素将是一个符号和DFA状态的配对。

因此,栈现在将是一个配对栈,而之前,栈将仅由符号组成,Sim 1到Sim n,现在我们将有相同的符号,但每一个都将与一个DFA状态配对,该DFA状态将是运行DFA的结果,在其左侧的所有符号上,因此。

在栈中低于它的所有符号,若我考虑栈,画栈为线,DFA在此状态,称此状态为,运行DFA栈结果,点左内容,再看栈中另一点,状态,存储的栈状态,运行DFA栈结果,联系内容至该点,底部有一个小细节。

我们必须开始,需要将起始状态存储在底部,只需用任何占位符存储。

选取的符号无关紧要,现在准备好详细解析算法,定义一张表,转到,映射一个状态,和一个符号到另一个状态,这就是dfa的转换函数,dfa的图以数组形式写出。

我们的slr解析算法将有四种可能动作,一个shift x动作将推入一对到栈上,X是dfa状态,现在命名为shift动作,而配对的另一元素是当前输入,然后我们也会有减少动作,和之前一样,回顾一下。

减少动作将弹出栈上一定数量的元素,等于右侧长度的元素,然后将左侧推入栈上,最后接受错误动作,用于成功解析输入,以及解析器卡住时。

第二个解析表是动作表,它告诉我们每个可能状态下应采取哪种动作,动作表由自动机的状态和下一个输入符号索引,可能的动作包括像shift,减少,接受,或错误,所以让我们考虑当我们做shift时。

如果栈顶自动机的最终状态,有一个项目说可以shift一个a并转到,这意味着从这个状态我们可以在输入a时转到状态j,那么在状态i上输入a的动作将是shift a j到栈上,想想这意味着什么。

这意味着我们有一个栈,然后下一个输入是a,然后在这个点上可以shift一个a到栈上,并且进一步说,当前自动机的状态si是ok的,栈顶自动机的状态是si,下一个输入是a,记住go to表是机器的转换函数。

所以如果我们移动竖线,如果我们shift那个a到栈上,我们不仅仅把a放到栈上,我们必须把一对放到栈上,问题是应该放置哪个机器状态,将从状态i到状态si达到的状态,输入a,在这种情况下。

goto表告诉我们状态是sj,因此,终止于状态i时的动作,下一个输入是a,将a,j对推入栈中,动作表中另外三个移动是我们已见过的,所以,如果自动机的最终状态,栈顶项表示我们可以归约,且后续条件满足。

即下一个输入可遵循,产生式左侧非终结符,则在状态si,输入a时,可按该产生式规约,X 归为 α,但有一个例外,不会进行该规约,若左侧为特殊开始符,添加到文法中的新开始符S',因为在这种情况下。

如果我们要减少的项是s的质数,它变为s点,并且我们到达输入的末尾,那么我们想要接受,任何其他情况都是错误,所以在任何其他情况下,如果我们处于状态i并且我们有下一个,下一个输入是一个井号。

我们不知道是否要移位,减少或接受,因此这是一个错误状态。

最终这里是完整的SLR解析算法,我将带你逐步了解,为了了解我们讨论的所有想法,所有碎片都完美契合,让我们把初始输入称为i,我们就给它取个名字,索引将被称作j,最初是零,因此我们指向输入流的第一个标记。

我们假设dfa的第一个状态称为状态一,这意味着我们的初始栈将包含状态一,对于自动机的状态,和一些我们不关心的其他占位符号在第一位置,因此栈只是一个包含它的对,表示我们在dfa的起始状态。

现在我们将重复以下循环,直到我们成功解析输入或检测到错误,那么在每一步我们要做什么呢,我们将查看下一个输入符号,并查看自动机的最终状态,栈内容,总是堆顶对的状态,将在动作表中查找这两项。

将告诉我们进行哪种移动,让我们按顺序查看移动,首先考虑移位移动,所以如果我们,如果说明我们应该移位并进入状态k,那么我们将要做的是,我们将移位输入,这意味着我们将获取下一个输入符号,抱歉。

并将它推入栈中,连同自动机状态k,这对将进入栈中,我们还将输入指针向前移动,这样我们正在查看下一个输入字符,让我擦掉它,这样您现在可以继续阅读,关于规约移动,这个有点有趣,首先我们要做的是。

我们将从栈中弹出一定数量的项,等于右侧长度的堆栈,我们将弹出与右侧等效数量的项,等于产生式的右侧,然后我们将什么推入栈中呢,我们将左侧的非终结符推入栈中,现在的问题是栈上的状态是什么,什么dfa状态。

但现在我们已经弹出栈,我们可以查看新的栈顶状态,所以dfa状态和现在的栈顶状态,在我们完成弹出后将告诉我们a的最终状态,栈中剩余的内容,现在我们将x推入栈中。

我们想了解dfa在标记x的转换上会进入什么状态,因此我们使用goto表进行查找,栈顶状态和符号x,dfa会去哪里,这就是被推入栈的状态,最后,如果移动是接受,我们正常停止,如果移动是错误。

我们停止并报告错误或执行错误恢复程序。

关于这个算法的一个有趣之处是它仅使用dfa状态和输入,栈符号并没有以任何真正有趣的方式使用,因此我们实际上可以消除栈符号,仅使用栈上的dfa状态进行解析,但那样,当然,意味着放弃程序。

我们实际上还需要程序的后期阶段,因此,为了类型检查和代码生成。

我们现在仍然需要保留符号,简单LR解析被称为简单是有原因的,实际上,实践中它有点太简单,广泛使用的自底向上解析算法基于更强大的语法类,称为LR语法,LR语法和SLR语法的基本区别在于。

向前看被内置到项中,那么这意味着,一个LR1项将是一个对,它由我们之前看到的项组成,这意味着与之前完全相同,以及向前看,在LR1项的情况下,只有1个向前看标记,如果是LR2项,那里可以有2个向前看标记。

这对的意义是,如果我们最终到达一个状态,我们已经看到了所有这些生产,这个生产的所有右侧,那么减少将是可行的,如果向前看在那时是美元,如果是输入的结束,当然,那里可以有任何其他标记,任何其他终端符号。

除了美元,这比仅仅使用后缀集更准确,回想一下,在SLR解析中做出减少决策的点,我们只是看左侧符号的整个后缀集,将向前看编码到项中的机制,允许我们跟踪更细的粒度,哪些向前看实际上可能在特定生产序列中。

如果你看你的解析器的自动机,实际上它不是LR1自动机,它是一个LALR1自动机,这是非常接近LR的东西,它是对LR的一个小优化,一个纯LR自动机,但无论如何,它使用完全相同的项。

带有标准LR零项和向前看的对,如果你看那个自动机,你会看到像这样的事项,那应该帮助你阅读自动机并弄清楚它在做什么。

P4:p04 02-01-_Cool_Overview - 加加zero - BV1Mb42177J7

你好,在接下来的视频中,我将概述一种酷语言,您将编写编译器的编程语言,Cool是面向对象的课堂语言,缩写当然是,酷,Cool的独特设计要求是,编译器必须在相对较短的时间内编写,时间。

学生写编译器仅一学期,因此,它必须快速实现,主要用于教学编译器,世界上的酷编译器远超酷程序,所以有很多,编写了许多编译器,成千上万的编译器,可能有数万酷编译器,但只有几十或几百个酷程序。

可能是唯一存在的语言,编译器数量超程序,但揭示主要设计要求,在酷中更重要,编译器易写,程序易写,语言有些怪癖,为实施简化,不损教学价值,日常使用不便,作为工作程序员,语言里有什么,嗯,呃。

我们尝试设计它,以给你现代抽象概念的味道,静态类型,通过继承重用,自动内存管理,实际上还有一些我们稍后会讨论的,但很多东西被省略了,我们无法将所有内容,放入语言并快速实现,嗯,讲座中会涵盖一些内容。

但不幸的是,甚至有些有趣的语言想法,我们无法在本课中涉及,因此课程项目是构建一个完整的编译器,特别是你将编译cool到mips汇编语言,为80年代设计的机器,有可在任何硬件上运行的MIPS模拟器。

这使得整个项目非常便携,运行你的编译器,生成MIPS汇编语言,然后同一语言可在任何机器上模拟,项目访问分为5个作业,首先编写一个酷程序,该程序本身将是一个解释器,以获得编写简单解释器的经验。

编译器本身将包括我们讨论的4个阶段,词法分析,语法分析,语义分析,和代码生成,所有这些阶段都是可插拔的,意味着我们有单独的实现,每个阶段的单独参考实现,例如,当您正在处理语义分析时,您可以将词法分析。

解析和代码生成组件从参考编译器中取出,并将您的语义分析插入该框架并测试参考组件,因此这样,如果您对一个组件有困难,或不确定您的某个组件工作得很好,您不会在处理另一个组件时遇到问题,因为您能够独立测试它。

最后没有必需的优化作业,但我们有一些建议的优化,许多人已经编写了优化,对于酷,这是一个可选的,作业,嗯,如果您对程序优化感兴趣。

让我们编写最简单的酷程序,首先要知道的是酷源文件,扩展名为。cl的cool,您可以使用任何编辑器编写程序,我恰好使用emacs,嗯,您可以使用其他编辑器,如果您喜欢。

每个酷程序都必须有一个名为main的类,让我们谈谈这一点,类声明以关键字class开始,后跟类名,因此在这种情况下main后跟一对花括号,花括号内是所有属于该类的内容,每个类声明必须以分号结束。

程序由类声明列表组成,每个类声明以分号结束,这就是类的结构,现在我们需要这个类做点事情,所以我们将在这个类中有一个方法,我们称其为main,事实上,主类的main方法必须始终存在,这是启动程序的方法。

此外,此方法必须不接受参数,因此,main方法的参数列表始终为空,并且让我们说main方法的主体始终位于一对花括号中,因此,main方法始终位于花括号中,类由这样的声明列表组成。

并且再次声明必须全部由分号分隔,所以和或终止,抱歉,由分号,在这种情况下,类中只有一个方法,但它仍然必须有其分号,现在我们可以说我们希望方法实际做什么,这是方法代码所在的地方。

让我们拥有最简单的可能方法,那个仅仅评估为数字1的方法,好的,Cool是一种表达式语言,这意味着,代码可以去的任何地方,你可以放置任意表达式,任何表达式都可以在那里,方法没有明确的返回语句。

它只是方法体的值,所以在这种情况下,我们只放入数字1,这将是运行此方法时的方法值,所以让我们保存它,现在我们可以尝试,呃,编译这个简单的程序,那么如何编译,编译器称为coc,用于Cool编译器。

你只需给Cool编译器一个列表,Cool源文件,所以在这种情况下只有一个文件,一个dot cl回车,哦,我们得到了一个语法错误,所以我们必须回来修复它,错误说在第三行的开花括号附近,有一个错误。

我知道错误是什么,因为我是有能力的Cool程序员,至少是有点能力的Cool程序员,至少有点能力的Cool程序员,方法必须声明返回类型,因此需要在此处添加类型,声明的语法是在参数列表中,方法名称后加冒号。

然后类型名称,由于此程序返回数字1,嗯,主方法也应返回整数,保存,回到编译窗口,再次编译成功,现在看目录,看到一新文件one。s,是程序one的汇编代码,现在可尝试运行代码,和,呃。

MIPS模拟器叫spin,仅需汇编文件模拟,所以给,one。s回车,它会运行并打印很多东西,但如你所见,它说程序成功执行了一半,那很好,然后有一些统计数据,如执行的指令数,加载和存储的次数,分支数。

如果我们担心性能,这些数据会很有趣,如果我们正在优化编译的代码,但我们现在不做这个,我们只是在运行程序,我们可以看到程序是否运行正常,程序运行成功结束,但并未实际输出任何内容,因为我们并未要求它输出。

若要输出,需返回修改程序,当前程序仅返回其值,但该值未做任何处理,未打印,或其他类似操作,若要在酷程序中打印内容,你必须明确这样做,所以有一个内置在原始类io的特殊类,我们可以声明一个。

称为此类属性的东西将是io属性,它将被称为i,i将是一个我们可以使用的对象,以执行I/O,嗯,我们可能添加一个调用out string,I。out string是我们调用方法的方式,好的。

字符串是io类的方法,我们用i调用该方法,然后我们可以传递一个字符串,我们想打印在屏幕上,例如,我们可以说你好,世界,好的,现在我们要决定,嗯,数字1怎么办,让我再展示一个cool的特性,让我们留下1。

并让它成为语句块的一部分,语句块由分号分隔的表达式序列组成,你可以有任意数量的表达式,语句块或表达式块的语义是,按顺序评估表达式,块的值是最后一个表达式的值,语句或表达式块必须包含在,自己的花括号中。

好的,现在是一个有效的cool程序,让我为你读一下,程序的主体是一个表达式块,第一个执行对i对象的out string调用,将为我们打印你好世界,然后第二个评估为1,这是整个方法的值,实际上。

我应该说它是块的值,好的,然后因为块是uh,方法的体,块的值成为整个方法的值,因此,此方法调用将返回1,所以让我们保存这个,回到这里,让我们再次编译,所以,看起来我未能保存它,让我们编译这个。

我们看到有一个语法错误,所以它说,在第4行,我们有一个语法错误,靠近关闭的花括号,问题是,语句块或表达式块由,一系列或序列的表达式终止于分号,我们忘了终止序列中的最后一个表达式,嗯,用它的分号。

所以我们要添加那个,现在我们应该能够编译这个,瞧,看哪,编译正确,然后可以运行,现在看到,哦,又出错了,我们有,呃,程序运行时抱怨我们有一个无效的分派,所以第4行,我们的分派指向一个不存在的对象。

可以看到对i的分派调用,它不存在,因为实际上我们忘了为i分配对象,所以这里声明i为io类型,但这并不实际创建任何对象,就像用户创建变量名,我呃,但i实际上没有值,所以如果我们要i有实际值。

我们必须初始化它为某些东西,所以我们可以初始化它为一个新的io对象,这里是你在cool中分配新对象的方式,new,总是需要一个类型参数,所以在这种情况下,我们正在创建一个新的io对象。

并将其分配给这个对象,i,注意这里i是i是i,在java中被称为字段名,我们在cool中称之为属性,所以这些都是数据,类的数据元素,所以类可以有名称,即属性,或持有值的字段,以及可以执行计算的方法。

所以让我们保存这个并切换回来,现在我们将再次编译,所以仍然编译,现在可以运行,现在运行,看哪,如您所见,在底部,从顶部第三行,打印出,Hello world,这看起来有点难看。

因为成功的执行消息与我们的Hello world消息在同一行,所以让我们修复它,让我们回到这里,在我们的字符串中我们可以添加一个新行,好的在字符串末尾,反斜杠n是你在字符串中写新行字符的方式。

保存它回到这里,让我们编译,如果你不知道,Unix Bang重复前一个表达式,以相同前缀开始的前一个命令,你在Bang后输入,所以我想运行以c开始的最后一个命令,即编译。

然后我想运行以s开始的最后一个命令。

即运行spin,现在我们可以看到啊,它都很漂亮,Hello world在一行上。

所以让我们继续,呃,让我们清除所有这一切,所以让我只给你展示同一个程序的几个变种,我在这里要做的就是重新编写它,以几种不同的方式,所以只是为了说明一些cool的特性,让你更熟悉语法。

同时也只是展示做同样事情的替代方法,所以你知道这个,呃,这里的表达式块有点笨拙,来实现Hello world程序,所以让我们去掉那个,呃,让我们去掉,呃,那个块,让我们去掉这里的,好的。

让我们只让语句体是一个表达式,现在我们要面对的问题是类型不匹配,但只是为了说明这一点,让我给你看,所以让我们做cool c of one dot cl,你会看到它抱怨推断出的返回类型。

I o of the method main不匹配声明的返回类型int,所以回到这里,回到程序,嗯,那个呃,编译器发现这个表达式i dot outstring,产生一个类型为io的对象。

所以它返回i对象作为评估这个表达式的结果,这与类型int不匹配,所以自然地编译器说嘿,类型有些问题,那很容易修复,我们可以只需更改main方法的返回类型,说它返回某种类型io。

所以让我们回到这里看看现在是否有效,所以我们编译程序,然后运行输出旋转,是的,一切仍按预期工作,我们不必过于具体类型,因为我们实际上没有使用方法体的结果,我的意思是程序一旦退出,它打印字符串。

我们可以在这里允许更多灵活性,我们可以简单地声明main的返回类型为object,所以object是类层次结构的根,其他所有类都是object的子类,所以让我们回到这里,让我们先保存这个。

然后我们可以回到编译窗口,我们可以编译它并运行它,它仍然工作,嗯,如果我们想,我们还可以做,我们可以观察到,我们声明的这个属性这个字段,在这里实际上是不必要的,我们分配,你知道,我们有一个特殊的名字。

当主对象构造时运行程序,一个新的io对象分配给i,然后那个在main方法中使用,我们实际上可以在main方法本身中做所有这些,只需在这里分配一个新的io对象,然后在那对象上调用out string好吧。

所以这应该也能工作,让我们检查一下,所以它编译了,瞧,看,它运行了好吧,所以回到这里,让我们再说明一两个我们可以做的事情,所以我们可以说类main继承自io。

所以我们必须在某个地方有io功能才能调用out string方法,我们一直在通过创建类型为io的单独对象来做这件事,但现在我们可以说好吧,主对象本身就是具有所有io能力的东西,通过继承io。

如果你以前见过任何面向对象的语言,这将是一个熟悉的概念,所以main这里得到了io的所有属性和方法,除了它自己将拥有的任何属性和方法,现在,我们不必为了调用out string而分配一个新的io对象。

我们可以直接在self上调用它,self是当前对象的名字,当main方法在其他语言中运行时,self被称为这个好吧,所以我们保存了它,现在让我们编译,编译成功,编译成功,运行正常,最后一个例子。

这里实际不必命名self,有特性允许调用方法,无需明确对象即可派发,默认是self,派发未命名对象,则派发给自己,这应该也行,确实如此。

结束第一个例子,接下来几视频,看更多酷编程例子。

P40:p40 08-08-_SLR_Examples - 加加zero - BV1Mb42177J7

本视频中,我们将处理几个SLR解析示例。

让我们做一个非常简单的例子,考虑语法S->Sa或S->b,这个语法做什么,它产生a的字符串,然后是b,任意数量的a后跟一个b,注意语法是左递归的,回忆一下,这对自底向上解析器不是问题,SLR解析器。

LR解析器完全接受左递归语法,所以让我们开始计算这个语法的自动机应该是什么,解析自动机应该是什么,回忆一下,第一步是向语法中添加一个新生产,我们必须添加一个新的开始符号,它只有一个生产,指向旧开始符号。

这又是出于技术原因,开始符号或抱歉,解析自动机NFA的起始状态是这一项S',我们的新开始符号指向点S,我们的旧开始符号,而不是构建NFA,然后进行状态子集构造,让我们直接计算。

DFA的第一个状态中必须包含哪些项,记住,在NFA中的所有epsilon移动,是由于移动发生,因为我们没有看到非终结符在栈上,但它说看到由该非终结符派生的东西,所以如果我们有一个点紧挨着一个非终结符。

这意味着在NFA中有epsilon移动,到所有那些对于该非终结符的所有生产,所有第一个项的所有生产,我指的是什么,我的意思是,这个状态,意味着epsilon生产到S->。Sa。

所以这是识别这个生产的第一个项,点在最左边,对于另一个生产也会有项,对于S S->。b,所以这是NFA中起始项的epsilon闭包,所以这将是第一个状态,这三件事,这三个项将是DFA的第一个状态。

现在我们必须考虑对于每个可能的转换,对于我们在栈上可能看到的每个符号,所以让我们想想如果我们看到一个b,所以如果我们看到一个b在栈上,所以如果我们在栈上看到一个b,那么状态中唯一项将是s到b点,好的。

所以看到a b是可以的,这将是栈内容唯一有效项,现在另一种可能是我们看到一个s,好的,所以如果我们在栈上看到s,会发生什么,我们将进入有两个项的状态,S撇,嗯,转到s点,所以我们在栈上看到了s。

我们准备通过这条产生式规约,可能还有s转到s点a,现在显然在这个状态,实际上,让我们谈谈这个下面的状态,底部的状态,没有更多的转换可能,那里只有一项,点都在最右边,这个状态已完全完成,右边的这一个,嗯。

其中一项已完成,所以最右边,但另一项仍有一个a,因此可能还有一次从这个状态到该项目的转换,S转到点,好吧,现在看看这个,我们看到大部分,这些州状况良好,这两个州,这个和这边这个,它们只有一项。

没有转移的可能性,减少这些州的冲突,只有一项,只有一件事要做,这两个州唯一的可能性是减少这个州,初始起始状态没有减少的移动,所以只有移位操作,因此没有移位归约冲突,因为没有归约项,无项,无可能归约动作。

同样原因,无归约归约冲突,所以感兴趣的唯一状态,实际上,从语法是否为SLR1的角度来看,是这个中间状态,好的,这里我们可以通过s'->s。归约,或者将a移入栈中,问题是什么跟在s'后面。

语法中s'后面能跟什么,如果我们回头看语法,会发现s'后面不能跟任何东西,s'是开始符号,实际上,s'后面的唯一东西是输入结束,这意味着如果输入结束,我们减少s'到s,否则如果有a在栈上。

抱歉如果有a在输入中,我们会把它移入栈中,所以这个语法是SLR1,没有移入,减少或减少冲突,由该解析自动机隐含的减少冲突。

让我们再做另一个稍微复杂的例子,实际上让我们扩展之前的语法,我们会有个产生式s到saas,所以现在s是非终结符两次,中间有个a,或者s可以到b,就像之前,现在让我们计算这个语法的解析自动机。

我们再次需要向语法中添加一个虚拟开始符号,它将,唯一的产生式是生成旧开始符号,现在让我们开始计算,解析自动机中这个特定语法的什么,和之前一样,我们不会费力构建NFA,那是系统化的一种方法。

一种方法是像我们草绘的那样,首先构建NFA,然后进行状态子集构造,但这个语法足够小,足够简单,我们可以直接计算,状态中有什么,DFA状态中的项是什么,和之前一样,因为点紧跟在s后面,我们知道我们可以。

不消耗任何输入,在NFA中做epsilon转移到达开始s的产生式,这些将在,也在DFA状态中,就是这样,我们不能在这里添加其他产生式,所以s是唯一的非终结符,我们已经添加了所有s的初始项,初始项。

所以这是完整的状态,好的,和之前一样,一种可能是我们在栈上看到b,那么这将给我们s到b点的项,该项仅适用于该状态,另一种可能是我们将在栈上看到s,然后确定,在这种情况下,我们将进行状态转换。

S'转到s点,S转到s点,S可以,我们在其他态中见过相同的状态,嗯,现在我们也可以看到a,那会带我们去哪个状态,这将有些不同,在这种状态下,我们可以有该项,或者我们会有该项s a点s。

但现在注意点紧跟在s后面,因此,我们不仅可以看到栈上的s,我们还可以看到栈上s的派生项在下一个位置,因此,我们必须加入所有s的生产式,只有两个,但这意味着我们可以有该项。

S转到点s as s和S转到点b,好吧,现在从这个状态开始,有几个不同的可能转换,我们可以看到s或b,如果我们看到b,那么我们最终会进入这个状态,如果看到s,那么,会发生什么,如果,如果我们看到s。

那么我们最终会进入另一个新状态,我们有了s goes to s a s点,我们看到了该生产式的完整右半部分或s goes to s a点s,嗯,实际上,那个位置不对,让我们擦掉它,把它放在正确的位置。

它在这里,在a之前,不是在a之后,好吧,现在我们必须考虑在这个状态下会发生什么,所以在这个状态下,唯一的可能输入是a,如果是a,会发生什么,我们将有s goes to s a点s。

然后我们必须再次添加s的初始生产式,那将把我们带回这个状态,让我把转换标记在这里,我们从s状态转到该状态,从该状态回到s状态,底部状态从顶部状态通过a,我想如果我们没有犯任何错误。

这就是完整的转换系统和该dfa的所有状态,现在的问题是,这是否是,这是否是slr 1文法的解析自动机,为了回答这个问题,我们必须寻找可能的,归约,归约和移位,妥善处理冲突,快速浏览所有州。

或让你相信没有两个可能减少的动作,因此,这里没有减少减少冲突,在这个自动机中,我们可以忽略只有一项的状态,或没有可能减少动作的状态,因为这些是不会有移位减少冲突的状态,这意味着我们可以忽略这两个状态。

最左边的两个状态,所以现在只剩下这三个状态要考虑,我们上次看的这个状态,嗯,之前s prime的跟随就是美元符号,所以没有移位,减少冲突在这个状态,因为在输入a时,我们只能移位,不能减少,嗯。

由s prime去s现在只剩下看这两个状态,让我们先考虑这个底部的状态,这个状态告诉我们做什么,这个状态说首先,注意从这个状态出去的唯一的转换是b和s,这个状态根本没有减少动作,所以没有移位。

减少冲突的可能性,要么,这就剩下这个状态要考虑,所以现在这个状态确实有减少动作,这里的第一个项目是一个减少,这意味着我们应该通过s去s a s减少,如果接下来的是s的跟随。

所以我们需要知道s的跟随是什么,从s prime去s,我们知道s prime的跟随也在s的跟随中,显然美元符号在s的跟随中,然后从这个语法部分我们可以看到a在s的跟随中,然后从这个s的出现,嗯。

我们知道因为它出现在生产规则的右边,那么右边的非终结符的跟随,也是左边的非终结符的跟随,在这个情况下它们是一样的,它只是说s的跟随是s的跟随的子集,这显然总是正确的,并没有添加什么新东西。

所以我们最终得到s的跟随就是这两个东西,美元符号和a,美元符号和a,但这带来一个问题,因为这说明如果输入有a,我们应该归约,而此移动说明如果输入有a,我们应该移位,因此此状态确实有移位,归约冲突,好的。

P41:p41 09_09-09-Implementing - 加加zero - BV1Mb42177J7

本视频将讨论,如何对类型进行检查,并将其转化为实现。

类型检查的高层概述,是它可以单次遍历,抽象语法树实现,实际上分为两个阶段,顶部向下阶段传递类型环境,底部向上阶段传递类型,从树的根部开始,递归传递初始类型环境,通过抽象语法树的各个节点,直到到达叶子。

从叶子开始,使用环境计算子表达式的类型。

回到树的根部,让我们从类型系统,中一个简单的规则开始,加法规则,简单回顾一下,类型检查e1+e2,首先类型检查e1,然后类型检查e2,两个子表达式必须是int类型,如果满足,则整体表达式。

两个子表达式的和也是int类型,类型检查在环境中进行,整个表达式和子表达式环境相同,提醒一下,始终存在对象环境,用于作用域内的对象名,类的方法环境,以及当前的类,如何实现。

将有一个递归函数type_check,它接受两个参数,类型环境,这是一个记录,不具体说明如何声明,但意思是,主要包含三个部分,O、M和C,还接受一个表达式,这里只处理e1+e2的情况,代码应该怎样。

可以直接根据规则翻译成代码,这是类型系统符号的好处,它非常清楚地告诉你如何从描述中编写实现,首先需要做什么,我们需要类型检查子表达式e一,从规则中可以看出,e一类型检查的环境。

与e一加e二的环境完全相同,我们只是将原始环境参数传递给e一加v二,将其作为参数传递给递归调用以类型检查以类型,检查子表达式e一,该类型检查将运行,它将返回某些类型t一,此时我们不知道t一是整数。

我们接下来将不得不检查,因此我们只记得e一的类型,此外,我们类型检查e二,好的,这也发生在相同的环境下,我们可以在规则中看到,嗯,我们还将获得一些类型,嗯,对于e二的类型t二,然后确认t一和t二,嗯。

都是整数类型,我们现在可以做,嗯,t一是int的检查,马上,在我们类型检查e一之后,在这里做这件事是很好的,我只是为了节省幻灯片上的空间,我把对t一和t二的检查,都放在了一行,如果这个检查成功。

如果它不成功,这里应该有一些代码来打印错误消息,但如果t一和t二实际上是整数,那么整个表达式的类型也是整数,这就是这个调用返回的。

由最外层的类型检查函数调用,现在让我们看一下稍微更复杂的类型检查规则,及其实现,这是let初始化的规则,我们正在声明一个变量x,类型为t,它将在表达式e一中可见,但在执行e一之前。

我们将初始化x为e零的值,然后在我们评估了整个let表达式之后,我们期望得到一些类型t一,所有这些都必须成功,一些事情必须得到满足,这些是规则的前提,首先,e零必须具有某些类型t零,它是t的子类型。

这是初始化正确的保证,这就是为什么这一切都必须奏效,x实际可持有e零类型,整个表达式为t一类型,则e一需为t一类型,但类型检查在扩展了x声明的环境中进行,因此我们也知道,对于e一,x为t类型。

现在让我们编写此的类型检查情况,因此函数类型检查再次,它将接受一个环境作为参数,现在我们在做let初始化的案例,所以只是读取,规则和我们必须检查的条件,我们可以看到,我们首先必须做的。

或我们必须做的是检查e零是否有某种类型t零,我们只需递归调用类型检查,这将在整个表达式的相同环境中进行,因此我们将环境传递给递归调用,我们现在正在类型检查e零,并记录其类型t零,第二个前提像这样实现。

现在,我们正在类型检查e一,并期望它具有某种类型t一,但现在环境不同,因此我们取原始环境,表达式的整体环境,并向该环境中添加x为t类型的声明,因此我们正在用额外的变量声明扩展环境,好的。

因此我们进行该类型检查调用,我们得到一个类型t一,现在我们必须检查t零是否是t的子类型,所以那是一个,那是一个实现子类型关系的函数的调用,如果通过,如果检查通过,那么我们就完成了,我们可以返回类型t一。

幻灯片上有一个小错误,那里应该有一个分号。

P42:p42 09-01-Introduction_to - 加加zero - BV1Mb42177J7

欢迎回到本视频,我们将做简短介绍,语义分析概述。

回顾编译器讨论,讨论了词法分析,从语言定义执行角度看,词法分析主要工作是检测输入,输入字符串,非语言单词或原始符号,下一步是解析,我们也结束了那话题,从判断程序是否正确,或是否有效程序的角度。

解析的任务是检测语言中,所有不正确的句子,或没有解析树的句子,最后我们要讨论的,现在将占据我们相当长一段时间的是语义分析,这是所谓的,前端阶段中的最后一个,逐步拒绝更多输入字符串的过滤器,最终只剩下。

所有三个阶段运行后,只有有效程序可编译,语义分析是最后一道防线,它是管道中的最后一个,它的工作是捕获程序中所有潜在的剩余错误。

现在你可能会问自己,我们为什么甚至需要一个单独的语义分析阶段,答案非常简单,编程语言有一些特性,有些错误解析无法捕获,解析,我们将使用,上下文无关文法不够表达,描述语言定义中我们感兴趣的一切,因此。

这些语言结构并不友好,情况非常相似,当我们从词法分析切换到解析时,就像不是,所有事情都能用有限自动机完成,我们想要更强大的东西,如上下文无关文法,以描述编程语言的其他特征,上下文无关文法本身也不够。

还有一些额外的特征,不能用上下文无关结构轻易表达。

那么语义分析在酷C的情况下实际做了什么?它进行了多种检查,这很典型,这是酷C执行的6类检查列表,让我们快速浏览一下它们,首先,检查所有标识符已声明,还要确保标识符的限制被遵守,酷编译器需类型检查。

这是语义分析器的核心功能,酷啊,来自面向对象特性的限制,检查类间继承关系是否合理,不希望类被重定义,每个类仅允许一个类定义,类似地,方法应在类内仅定义一次,酷有若干保留标识符,需小心勿误用,这很典型。

许多语言都有保留标识符,需遵循特殊规则,实际上此列表不完整,还有许多其他限制,将在后续视频中讨论,主要信息是语义分析器需执行,多种不同检查,这些检查随语言而异。

酷C执行的检查是静态类型检查面向对象语言的典型,但其他语言家族将有不同检查。

P43:p43 09-02-_Scope - 加加zero - BV1Mb42177J7

欢迎回到本视频,我们将从范围话题开始讨论语义分析。

讨论范围的动机是,我们希望能够匹配标识符声明,与那些标识符的使用,当我们说变量x时,需要知道指的是哪个变量,如果变量x,在程序中可能有多个定义,这是大多数编程语言中的重要静态分析步骤,包括在cool中。

以下是来自cool的几个例子,这个y的定义,这个y的声明,它将与这个使用匹配,因此,在这里我们知道y应该是一个字符串,你将从编译器中得到某种错误,因为你试图将字符串和数字相加,在第二个例子中。

这里是y的声明,然后在let的主体中我们没有,我们没有看到y的使用,这本身不是一个错误,声明一个不使用的变量是完全正常的,尽管你可以想象为它生成一个警告,这实际上并没有使程序表现糟糕,但相反。

我们在这里看到的是对x的使用,但没有匹配的定义,问题是x的定义在哪里,我们看不到,如果没有x的外部定义,那么我们将在这里得到一个未定义或未声明的变量错误。

这两个例子说明了范围的概念,标识符的范围是程序的一部分,在该部分中标识符是可访问的,只需知道同一个标识符可能在程序的不同部分指代不同的事物,相同的名称的不同范围不能重叠,因此,无论变量x,例如。

意味着它只能在程序的任何给定部分指代一件事物,标识符可以有受限的范围,有很多例子,我相信你熟悉它们,标识符的范围小于整个程序的例子。

当今大多数编程语言都有所谓的静态范围,cool是一个静态范围语言的例子,静态范围的特征是变量的范围仅取决于程序文本,而不是任何类型的运行时行为,程序实际上在运行时做什么并不重要,范围纯粹从语法上定义。

从你编写程序的方式,现在,如果有任何静态范围的替代方案可能会让人感到惊讶,实际上,你使用过的每种语言可能都有静态作用域,但有一些语言是所谓的动态作用域,很长一段时间。

实际上曾有过关于静态作用域是否优于动态作用域的争论,尽管今天,我认为很明显静态作用域阵营已经赢得了这场讨论,但至少在历史上,Lisp是一个动态作用域的语言,它在一段时间内已经切换,实际上。

它改为静态作用域已经是很久以前的事了,一种现主要为历史语言,不再真正使用,称为雪球,也具有动态作用域,和,动态作用域特征,是变量范围取决于程序执行。

让我们看静态作用域示例,这里有些酷代码和,呃,几个不同x声明,还有一些x的不同用法,让我擦掉这些下划线,这样我就可以用颜色表示绑定,让我们看看这个定义,问题是这些x的哪些用法,我们有3种x的用法。

这里实际上指的是那个定义,实际上就是这两个,这些实际上是指入口外的,这些实际上是指这个定义,这里若指x,则得值为0,但此定义,x的内定义用于此x使用,此x使用得此值,x的含义,在此情况下返回值为1。

正在发生的是,我们使用最接近的规则,因此变量绑定到最接近的相同名称的定义,这就是x最近定义,但这两个x最近,唯一包含定义是外层。

所以在动态语言中,变量将引用程序执行中最近的绑定,意味着变量最近绑定,所以这是一个例子,假设我们有函数g,g定义变量a,这里初始化,比如为4,然后它调用另一个函数,另一个不在同一语法范围的函数。

所以我把f写在了g旁边,但实际上f可能在代码的完全其他部分,f引用了一个,问题是这里的a的值是什么,嗯,如果是动态作用域,那么它将取g中定义的值,这里f(x)实际上将返回for,这将是这个电话的结果。

因为这个引用将指向这个绑定,或这个在g中的a定义,关于高动态,我们无法多说,动态作用域如何工作,在我们更详细地讨论语言如何实现之前,稍后我们还会再次讨论动态作用域。

在课程中,酷标识符绑定由多种机制引入,有类声明,引入类名,方法定义,引入方法名,然后有几种不同方式引入对象标识符,这些是引导表达式,函数的形参,类中的属性定义。

最后在case表达式的分支中,现在,重要的是理解,并非所有标识符都遵循之前概述的最内层规则,例如,这条规则的一个相当大的例外是cool中的类定义,因此类定义不能嵌套,实际上它们在程序中全局可见。

这意味着类名在程序的任何地方都被定义,如果它在程序的任何地方被定义,该类名可用于任何地方,在程序的任何地方或整个程序中,特别是类名可以在定义之前使用。

因此例如,我们声明y为bar类型,稍后我们声明类bar,这是完全合法的酷代码,bar在定义前被使用,不影响程序正确性,这是完全合法的酷代码,类似地,对于属性名,属性名在定义它们的类中是全局性的。

这意味着它们可以在定义前再次使用,例如,我可以定义类,定义方法用属性a,稍后,稍后,定义属性a?完全合法,通常先定义属性再定义方法,不是必须,类内方法属性定义顺序随意,属性可先使用后定义。

最后,方法名规则复杂,例如,方法不必在用到的类中定义,可在父类中定义,方法可重定义,可实现方法覆盖,给方法新定义,即使已定义过,目前无精确语言描述规则。

未来视频会深入讨论。

P44:p44 09-03-_Symbol_Tables - 加加zero - BV1Mb42177J7

本视频中,将讨论简单表和许多编译器中的重要数据结构。

在讨论简单表之前,我想谈谈一个通用算法,我们将在整个课程中不断看到其实例,因此,许多语义分析,实际上,许多代码生成都可以表示为抽象语法树的递归下降,基本思想是,在每个步骤我们执行以下三个操作,步骤如下。

我们总是在处理树中的一个节点,所以,如果我画一棵抽象语法树的图,我们可能有一个节点和一些子树挂在它上面,我们可能会对节点进行一些处理,在,做其他任何事情之前,我们到达节点,比如说。

我们从父母那里进来听父母的,我们对节点进行一些处理,我只是通过把它涂成蓝色来表示,表示我们在这里做了某事,然后我们处理子节点,好的,处理完子节点后,回到节点后,嗯,再做其他事,可能对节点进行后处理。

然后返回,同时处理其他节点,我们下车处理孩子,我们以相同预定的方式处理所有节点,因此,他们以相同的方式对待,每个节点之前都做了一些事情,所有孩子处理完后,再做些事情,这种算法的例子很多。

这称为树的自顶向下遍历,有些情况下,我们只会在处理孩子之前处理每个节点,有些情况下,我们只会在处理所有孩子之后处理每个节点,有些地方我们这样做,回到视频主题,在语义分析抽象语法树部分,需要知道,嗯。

哪些标识符已定义,哪些标识符在作用域内。

递归下降策略示例,跟踪作用域内的变量集,我们有抽象语法树中的let节点,在一个子树中有初始化,另一子树中有e,let的主体,这是一个特定变量的let,在父节点内写入该变量,因此,当我们开始处理此节点时。

假设我们从上面来,我们正在这样做,我们递归处理抽象语法树,因此,我们从某个父节点到达这一点,将有一组当前在作用域中的符号,这是一个位于侧面的数据结构,实际上,那将是我们的符号表,这里会发生什么,嗯。

首先,我们不得不做的是,我们不得不处理初始化器,我们需要知道是否,这与我们在此执行的任何函数有关,例如类型检查或其他我们可能首先处理的内容,我们将符号表传递进去,好的,然后,我们将处理let的主体。

但当我们这样做时,我们将传递在作用域中的符号集,但x现在也在作用域中,因此在处理e之前,我们将x添加到符号集中,然后,当我们从子表达式e返回时,它将删除,因此,我们将符号表恢复到先前的状态,因此。

在我们离开抽象语法树的此子树后。

我们只有与进入它之前相同的符号定义,在递归下降三步算法的术语中,我们在第一张幻灯片上看到的,我们在这里做什么?在处理e之前,我们将x的定义添加到我们当前的定义列表中。

覆盖lead表达式之外可能可见的任何x的定义,然后,我们将递归,我们将处理let主体中的所有抽象语法树节点,在e内部,在我们完成处理e之后,符号表只是一种实现这些功能的数据结构。

它跟踪抽象语法树中每个标识符的当前绑定。

对于非常简单的符号表,我们只需使用栈,它将具有以下三个操作,我们可以将符号添加到符号表中,这将仅将符号推入符号表,将变量推入栈中,以及我们想要的其他信息,例如类型,我们将有一个查找符号操作。

它将查找符号的当前定义,只需搜索栈即可完成,从顶部开始查找变量名首次出现,这将自动处理所有定义的隐藏,例如,如果我们有一个栈,比如有x,Y和z在上面,然后我们进入一个引入新y的范围,我们将y推至顶部。

现在如果我们搜索栈,我们会先找到这个y,有效地隐藏了旧的y定义,然后当我们离开一个范围时,我们可以简单地通过弹出栈来删除符号,我们只需弹出当前变量即可,这将消除最近的定义,并留下栈,将定义集恢复到。

进入节点之前的状态,所以在这个例子中,如果我们离开了定义外层y的范围,并且它从栈中弹出,那么现在它已经消失了,当我们搜索y时,我们会找到外层定义,在内部范围之外定义的那个。

所以这个简单的符号表对let很好用,因为符号一次添加一个,并且声明是完全嵌套的,事实上,声明完全嵌套的事实,是我们能够使用栈的真正原因,看看这个小例子,假设我们有三个嵌套的let。

在这里我没有显示左子树中的初始化器,它们不重要,对于我想说明的,所以如果你考虑一下,当我们从根部这里向下走到内部绑定时,我们正在将东西推入栈中,我们将按顺序将东西推入栈中x y和z,然后当我们离开时。

当我们处理完这个子树时,当我们走回去离开时,我们将遇到这些let范围,完全按相反顺序弹出它们,栈的顺序正是我们希望删除它们的顺序,这就是为什么栈工作得很好,所以这个结构对let很好,呃。

对于其他一些构造,呃,但并不像它可以的那样好,例如,考虑以下代码片段,非法代码片段,我应该添加,假设我们声明一个方法,它有2个名为x的参数,这是非法的,为了检测它不合法,为什么不合法,不合法。

因为它们都在同一范围内定义,因此,函数或方法具有一次引入同一范围内多个名称的性质,使用栈并不容易,我们一次只添加一个东西,或一次只添加一个名称,难以模拟范围内的同时定义。

这个问题很容易解决,但需要一个稍微复杂一点的简单表格,现在有5个方法而不是3个的修订接口,最大的变化是我们现在有显式的进入和退出范围函数,因此,这些函数开始一个新的嵌套范围并退出当前范围,可以这样认为。

我们的新结构是一个范围堆栈,堆栈上的东西是一个完整的范围,然后在范围内,是定义在同一级别内的所有变量,所以就像以前一样,我们有一个查找符号的操作,它将返回当前定义或null。

如果在当前可用的任何范围内都没有定义,我们将有一个添加符号的操作,它将添加一个新的符号到表中,它添加到当前范围,因此,我们范围堆栈顶部的任何范围,然后还有一个新的操作checkscope将返回true。

如果x已经在当前范围内定义,所以明确地说,这个操作返回true,如果x恰好在最顶层的范围内定义,它不会返回true,除非x在堆栈顶部范围的顶部定义。

这允许您检查双重定义,例如,在我之前幻灯片上的代码中,如果有两个x的声明,我们如何检查这个?我们将x添加到当前范围的符号表中,然后我们会问,x是否已经在该范围内定义(对于第二个)。

这个接口将返回true,我们会知道要引发错误,说x已被多次定义,最后,我只想说这是简单表格接口,或非常接近这个的简单表格接口,是随酷项目提供的,已经提供了一个该接口的实现,如果您不想自己编写。

所以让我们通过谈论类名来结束这个视频,类名,与let绑定和函数参数中引入的变量不同,特别是类名可以在定义前使用,如我们前几集讨论的,这意味着我们不能在一次遍历中检查类名,我们不能只遍历一次程序。

检查使用的每个类是否定义,因为我们不知道是否看到了所有类的定义,直到我们到达程序的末尾,所以有一个解决方案,我们需要对程序进行两次遍历,在第一遍中,我们收集所有遇到的类定义,我们查找所有定义类的位置。

记录所有那些名称,在第二遍中,我们查看类的主体,确保它们仅使用已定义的类,这里的教训是,实际上并不难实现,我认为这应该很清楚或应该很清楚如何工作,但这里的信息是,语义分析将需要多次遍历,可能不止两次。

事实上,编译器结构不应畏惧,增加大量简单步骤,若能简化生活,最好将问题拆分为,如三四个简单步骤,而非一个极,其复杂步骤,所有代码纠缠,你会发现调试编译器更容易,若愿意多次遍历输入。

P45:p45 09-04-_Types - 加加zero - BV1Mb42177J7

欢迎回到本视频。

我们将介绍类型。

一个基本问题是类型是什么,这个问题值得问,因为类型的概念因编程语言而异,大致来说,共识是类型是一组值,更重要的是,一组对这些值独有的操作,一组在这些值上定义的操作,所以,例如,如果我看整数的类型。

你可以对整数执行一些操作,你可以做加法,你可以减整数,你可以比较整数是否大于等于或小于,好的,然后这些操作是,关于数字的,然后字符串有不同类型的操作,它们有像连接和测试的操作,一个字符串是否为空。

你知道还有很多其他函数定义在字符串上,重要的是这些操作不同于整数上定义的操作,我们不想混淆它们,如果我们开始对整数执行字符串操作,例如,我们只会得到无意义的结果,在现代编程语言中,类型以多种方式表达。

在面向对象的语言中,我们经常看到类是类型的概念,所以特别是在Cool中,类名是类型,除了一个例外叫self类型,类名就是类型,我只想指出这不一定是这样,在面向对象语言中,将类和类型等同起来往往很方便。

但还有其他设计,其中类不是唯一的类型,或者它们不是,在一些没有类概念的语言中,类型是完全不同的事物,所以类和类型实际上是两个不同的事物,在大量面向对象语言设计中被识别。

我只想让你知道这不一定是唯一的做法。

考虑汇编语言片段,我加r一,r二,r三,这实际上做了什么,将寄存器r二的值和寄存器r三的值,相加,并将结果放入寄存器r一,问题是r一的类型是什么,r二和r三,你可能希望它们是整数,但实际上这是一个。

这是一个陷阱问题,因为在汇编语言层面,我无法分辨,没有任何东西阻止r一,r二和r三具有任意类型,它们可以是,它们可以是任何类型的代表,因为它们只是一堆包含零和一的寄存器,加法操作将乐于接受它们并相加。

即使没有意义,并产生一个位模式存储到r一,为了使这个更清楚,也许考虑某些对每种类型值合法的操作是有用的,例如,将两个整数相加是完全有意义的,如果我有两个代表整数的位模式,那么当我将它们相加时。

我将得到一个代表这两个整数之和的位模式,但另一方面,如果我取一个函数指针和一个整数,并将它们相加,我真的没有得到任何东西,这是另一个,函数指针是一个位模式,整数是一个位模式,我可以取这两个位模式。

我可以运行它们并通过加法,我确实得到了一个新的位集,但对这个结果没有有用的解释,我得到的结果没有任何意义,但问题是,这两个在汇编语言层面上具有相同的实现,好的,汇编语言层面,这两个操作看起来完全一样。

因此,在汇编语言层面上我无法分辨,我正在做的是哪一个,如果我想有类型,如果我想确保我只对正确的,我只对正确的类型执行某些操作。

那么我需要某种类型描述和一些类型的系统来强制这些区别,所以也许我正在强调这一点,但我觉得这很重要,所以再一次,语言类型系统指定了哪些操作对于哪些类型是有效的,然后类型检查的目标是确保操作仅与。

仅与正确的类型一起使用,只有与正确的类型一起使用,通过类型检查确保值解释,机器码层面无其他检查,仅是许多零和一,机器将执行我们告诉它的操作,无论操作是否合理,类型系统的目的是确保位模式解释。

确保整数位模式不被误用,避免得到无意义结果。

当前编程语言分三类,嗯,关于类型处理,有静态类型语言,编译时检查所有或几乎所有类型,Cool是其中之一,C和Java等语言也是静态类型,然后是动态类型语言,运行时检查几乎所有类型。

Lisp家族语言如Scheme和Lisp在此列,如Python和Perl等我们的语言,你可能使用或听说过至少其中一些语言,最后是无类型语言,完全不检查类型,编译时或运行时,机器码基本如此。

机器码无类型概念,不强制抽象边界,运行数十年。

关于静态与动态类型优劣有争论,不偏袒任何一方,为你列出,各派支持者所说,支持静态类型的人认为,静态检查在编译时捕获许多编程错误,也避免了运行时类型检查的开销,如果在编译时做了所有类型检查,那么。

运行时无需检查类型,进行操作时无需检查,参数是否为正确类型,因为在编译时已彻底检查一次,这些都是绝对正确的,这是静态检查的两个主要优势,首先,证明有些错误不会发生,这些在编译时捕获。

因此我无需担心运行时错误,而且更快,动态类型支持者反驳静态类型系统限制性,本质上静态类型系统必须证明程序类型良好,所有类型有意义,它通过限制可编写的程序类型实现,一些程序在静态类型语言中更难编写。

编译器难以证明其正确,普遍认为快速原型更难,使用静态类型系统,这里的意思是,如果你在原型,如果你在探索某个想法,你可能并不确切知道所有类型,必须承诺某种在所有情况下都能工作的东西,你知道。

有一个类型正确的程序,当你只是在摆弄,并弄清楚你要做什么,这限制很大,工作会慢很多。

那么实际现状如何呢,很多代码是用静态类型语言编写的,人们常用的实用类型语言总有一种逃逸机制,所以在C和Java中,C++,你有一些不安全转换的概念,在C中,不安全转换可能导致运行时崩溃,在Java中。

会导致运行时未捕获异常,当你有不安全或失败的向下转型时,但结果是,现在会因为类型原因出现运行时错误,在动态类型方面,使用动态语言编程的人,他们最终或似乎最终,将静态类型回溯到这些动态类型语言中,因此。

如果动态类型语言变得足够流行,人们开始尝试为它们编写优化编译器,人们想要优化编译器的第一件事是一些类型信息,因为它有助于生成更好的代码,因此,人们最终,回去,尝试弄清如何获取更多类型。

来自这些动态类型语言,一旦他们开始尝试构建,在我看来,是否妥协值得商榷,因为两者都是妥协,静电,或严格动态观点,但这就是我们现在的情况。

实际上,现在Cool是一种静态类型语言,Cool中可用的类型是类名,因此每次你定义一个类,你就定义了一个新类型,以及特殊的保留符号self类型,我们将在单独的视频中讨论它,它自己。

Cool的工作方式是用户声明标识符的类型,对于每个标识符,你需要说明其类型,但编译器完成其余工作,编译器推断表达式的类型,特别是编译器为程序中的每个单个表达式分配类型,我们将遍历整个抽象语法树。

使用标识符的声明类型,它将计算一个类型,嗯。

对于每个表达式和子表达式,总结,值得提及的是,对于计算类型的过程,人们使用了一些不同的术语,它们意味着略有不同,所以更简单的问题是这里所知的类型检查,我们有一个完全类型的程序。

意味着我们有一个抽象语法树,所有节点上都填满了类型,我们唯一的工作是检查类型是否正确,所以我们可以只看每个节点和它的邻居,并确认该部分的类型是正确的,我们可以对树中的每个部分这样做。

并检查程序是否正确类型推断,另一方面,是填充缺失类型信息的过程,所以这里的观点是我们有一个抽象语法树,上面没有类型,或者可能只有一些关键位置的类型,比如声明的变量,然后我们想要填充缺失的类型。

我们有一些节点完全没有类型信息,不仅仅是确认或检查类型是否正确,我们实际上必须填充缺失的类型信息,这两件事是不同的,实际上它们,实际上在许多语言中是非常,非常不同的,但人们经常交替使用这些术语。

我也不会在我的视频中特别小心使用哪个术语。

P46:p46 09-05-_Type_Checking - 加加zero - BV1Mb42177J7

本视频将讨论类型检查。

在编译器中,酷,到目前为止,我们已看到两种形式化表示,用于词法分析和解析的上下文无关文法,实际上,还有一种形式已被广泛接受,用于类型检查的逻辑推理规则。

推理规则是逻辑陈述,如果某个假设为真,则某个结论为真,因此,推理规则是蕴含语句,在类型检查中,示例,我们看到的典型推理,如果两个表达式具有某些类型,则另一个表达式保证具有某种类型,显然。

类型检查语句是推理规则的示例,推理规则表示法是编码这些。

如果-那么语句的简洁方式,如果你以前没见过这种表示法,它会很陌生,但实际上,通过练习很容易读懂,我们将从一个非常简单的系统开始,逐渐添加功能,我们将使用逻辑与表示英语单词,x:t表示x的类型为t。

这是一个逻辑断言,表示x具有特定类型。

现在考虑以下非常简单的类型规则,如果e1的类型为int,e2的类型为,则e1+e2的类型也为int,我们可以使用上一页的定义,逐渐将其简化为数学陈述,例如,我们可以将如果-那么替换为蕴含。

并将单词和替换为与,现在只剩下这些类型声明,我们有一种表示法,最终得到这个纯数学陈述,表示与e2类型为int的e1类型为int。

意味着e1+e2的类型为int,注意,我们刚刚写出的陈述是推理规则的特殊情况,是一组假设可以联合起来推导出某个结论。

推理规则的常规表示如下,假设写在水平线以上,结论写在水平线以下,与上一页完全相同的意思,即如果水平线以上的所有东西都为真,这些都是假设,则水平线以下的东西可以推断为真,这里有一种新的标记法。

这是用于假设的转义符,结论和转义符被读取,这是可证明的,这意味着我们明确表示某件事是可证明的,在我们定义的规则系统中,所以你会这样读,如果所有这些假设都是可证明的,如果它是可证明的,第一个假设是真的。

所有中间假设,如果最后一个假设是可证明的,那么结论是真的,酷的类型规则将有,以下类型的假设和结论将在系统中证明,某个表达式具有特定类型。

有了这些定义,我们实际上有足够的东西来写至少一些简单的类型规则,如果i是整数字面量,如果它是程序中的整数常数,那么这条规则说i的类型是可证明的,每个整数常数都有类型int。

这是现在用推理规则表示的加法规则,如果e一类型是可证明的int,并且e二类型是可证明的int,那么e一加e二类型是可证明的int。

注意这些规则为描述如何类型化整数和表达式提供了模板,整数常数的规则只使用了一个通用的整数,它没有为每个可能的整数提供单独的规则,加法的规则使用了表达式,e一和e二,它没有告诉你什么特定的表达式。

它们是什么,它只是说,给我任何表达式,e一,任何表达式,e一和e二,它们类型是int,所以我们可以插入任何我们想要的表达式,满足假设,然后我们可以为实际表达式产生完整的类型化。

所以作为一个具体例子,让我们展示一加二类型是int,我们想类型化表达式一加二,因为我们知道加法的规则,我们需要构建数字一的类型证明,和数字二的类型证明,我们有一个处理整数常数的规则。

即我们可以证明因为一是整数常数所以类型是int,我们可以证明二是类型int,现在我们有需要的两个假设,我们可以证明1加2的类型为in。

任何合理类型系统的关键属性是它必须是正确的,这是一个正确性条件,我们希望类型系统能证明的表达式具有特定类型,那么如果我实际运行该程序,如果我取e并在计算机上执行,返回的值,运行后返回的值。

实际上具有类型系统预测的类型,所以,如果类型系统能够给出反映运行程序时实际获得值的类型的东西,那么我们可以说类型系统是正确的,显然我们只想要正确的规则,但一些正确的规则实际上比其他规则更好,例如。

如果我有一个整数字面量,并且我想给它一个类型,虽然我之前给你看了最好的可能规则,我们说过i的类型为int,但仅仅说i的类型为object也是正确的,只是不太精确,当然如果我评估一个整数。

我会得到一个对象,因为每个整数和cool都是对象,但这并不十分有用,因为现在我不能做任何整数操作,所以有很多不同的正确规则,对于给定的cool表达式,并不只有一种唯一的正确规则,但其中一些比其他更好。

在整数字面量的例子中,我们真正想要的是整数字面量具有类型int,因为这是我们可以给那种程序的最具体类型,总结:类型检查证明e具有类型t的事实。

注意,这种证明是基于抽象语法树的结构的,所以对于表达式1加2,我们证明了关于1加2的一些事情,但首先证明了关于每个子表达式的一些事情,所以我们证明了子表达式具有类型in。

然后我们设法证明了整个东西具有类型in,好的,因此,证明与抽象语法树的形状相同,你可以把这个证明看作是一棵树,现在证明树的根在底部,我们通常将抽象语法树以顶部为根绘制,所以这棵树看起来像这样。

而我们通常以相反的方式绘制抽象语法树,但重要的是证明具有抽象语法树的形状。

对于每个抽象语法树节点,使用一个类型规则,总结:类型检查证明e具有类型t的事实,所以,证明结构与抽象语法树形状直接对应,抽象语法树特定节点的类型规则,将用于该节点的假设是这些子表达式的证明,因此。

构成E的任何表达式,我们首先需要它们的类型,在该特定节点上的结论将是整个表达式e的类型,这样,你可以看到类型是自下而上计算的,遍历抽象语法树,那就是,我将第一类型分配给叶子,像这样,我知道一个已输入。

两个已输入,然后类型流向根部,我能计算,然后下一级的抽象进入x树等,一旦我计算了节点所有子表达式的类型。

P47:p47 09-06-_Type_Environment - 加加zero - BV1Mb42177J7

本视频中,我们将继续开发酷类型检查,讨论类型环境。

让我们先做更多类型规则,这是常量false的规则,可证明常量false的类型为bool,并不意外,如果有字符串字面量s,可证明其类型为string。

也不意外,表达式new t产生类型为t的对象,类型规则很简单,new t的类型为t,暂时忽略self类型,如早期视频所述,稍后视频将处理self类型。

单独讨论,这里有一些更多规则,若表达式e的类型为bool,则布尔补e非e的类型也为bool,最后可能是目前最复杂的规则,while循环的规则,回忆e1是循环的谓词,决定是否继续执行循环,e2是循环体。

e1要求类型为bool,需证明e1的类型为bool,循环体e2的类型可以是任意,可以是类型t,必须有类型,遵循某些规则,但类型不重要,整个表达式的类型为object,不返回有趣值,不产生有趣值。

为了阻止依赖,整个类型为object,这是一个设计决定,现在,我们可以设计语言,例如,while循环的类型为t,你将得到循环最后执行的值的类型,问题是如果e1,循环的谓词首次进入循环时为false。

则从未评估e2,无值产生,在这种情况下,你将得到一个void值,这是设计问题,如果e1,循环的谓词首次进入循环时为false,则从未评估e2,无值产生,若有人尝试解引用,将导致运行时错误。

为阻止程序员在循环中,说谎产生有意义值。

可将其类型设为对象,到目前为止,为每个已查看的结构,定义合理类型规则,已很简单,但现在我们遇到问题,假设有一个仅包含,单个变量名的表达式,这是一个完全有效的表达式,问题是该变量的类型,称为x,如你所见。

仅查看x本身,没有足够信息给x类型,局部结构规则不包含,关于x类型的信息,推理规则具有属性,所有信息都需要是局部的,执行规则所需的一切,必须在规则本身中存在,没有外部数据结构。

没有传递的东西,都在一边,所有信息都必须编码在规则中,到目前为止我们还不清楚,变量类型应如何说明,解决方案是向规则中添加更多信息,这正是我们要做的,类型环境为自由变量提供类型,什么是自由变量。

如果在表达式中,一个变量未在该表达式中定义,则该变量为自由变量,例如,在表达式x中,x是x加y中的自由变量,这个表达式使用x和y,但该表达式中没有x或y的定义,因此x和y是该表达式的自由变量。

如果我有一个let y。所以在x加y中声明了一个变量y,那么,这个表达式中什么是自由的,这个表达式使用x和y,但y的使用由该表达式内部的y定义控制,所以我们说这里y是绑定的,若告知x类型。

可类型检查x+y,若告知x和y类型,可类型检查此表达式,此导引表达式,若告知x自由变量类型,y类型由let声明给出,仍需告知x类型,自由变量为需提供信息的变量,类型环境编码此信息。

类型环境为对象标识符到类型的函数,从变量名到类型。

设o为类型环境,这些从对象标识符到类型的函数之一,类型,现在将扩展,证明逻辑语句类型如下,此将被解读为,在变量类型由o给出的假设下,O为假设,置于转义符左侧,关于e中自由变量的假设,在假设。

自由变量类型由,O给出下,可证明,表达式e具有类型t,此符号很好地区分假设,这是确定类型所需输入,从证明的内容,若告知自由变量类型由,O给出,则可告知e类型。

类型环境需添加到所有现有规则中,例如,整数字面量,若对变量类型有假设,实际上不会改变,不会,实际上不会改变整数字面量的类型,任何整数字面量仍具有类型int,在这种情况下,对于这种特定表达式。

我们不会使用关于变量类型的假设,对于加法表达式情况稍有不同,若有表达式e1+e2,和关于变量类型的一些假设o,则想证明e1具有类型int,将使用,由o给出的变量类型,e1可能包含自由变量。

将需要在o中查找以确定这些变量的类型,类似地对于e2,将在相同假设下类型检查e2,如果e,在o和e假设下输入,在o well假设下输入2,那么我可以推断e,1加e 2在相同假设o下输入。

也能编写新规则,自由变量问题现成易解,若想知道x类型,这里缺o,若想知道x类型,在对象环境中查找,在变量类型由o给出的假设下,x类型是什么,嗯,在。查找,哦,x假设类型,然后可证明x有该类型,T。

现在看一个有趣变量规则,从环境角度看,这是let表达式,回顾let作用,无初始化let表达式,x是新变量,类型t0,变量在e1中可见,如何类型检查e1,在某种环境中类型检查e1,这是新记号,定义含义。

总是函数,映射变量名到类型,o t x是函数,o在x点修改的函数,返回t,整个函数,下划线部分是一个函数,应用于x返回t,这组假设,x类型t,其他变量,若应用于其他变量y,x不同于y,则。

得到y在o的类型,好,规则说在相同环境,o,除了x类型为t0,改变x类型为t0,因为e1中绑定新标识符类型,其他类型不变,用这些假设尝试证明e1有类型,将得到e1类型,然后整个let表达式的类型。

注意类型环境的一些特点,这表示在类型检查e one之前,我们需要修改假设集,并修改类型环境以包含关于x的新假设,然后类型检查e one,当然,当我们离开类型检查e one时,我们将删除关于x的假设。

那个新假设,因为let之外,我们只有原始的假设集o,所以,我希望那个术语和那个描述能让你想起我们之前讨论过的东西,因为这种类型环境实际上是由简单的表格实现的,所以,在我们的规则中,类型环境携带了将在。

或通常存储在编译器的符号表中的信息。

总结一下这段视频,类型环境为当前范围的免费标识符提供类型,这非常重要,因为如果没有关于免费标识符类型的信息,谈论类型检查和表达式,实际上没有意义,类型环境只是正式化,给一些假设起名字。

关于那些免费标识符的类型,注意类型环境从抽象语法树的根部向下传递到叶子,即,当我们通过定义时,类型环境用新的定义扩展,例如,和let表达式,因此,当您从抽象语法树的根部向下传递时。

类型环境将随着您向抽象语法树的叶子移动而增长,然后类型从抽象语法树的叶子向上计算到根,所以我们从叶子开始,获取所有类型,叶表达式,大多数都非常简单,像整数和字符串常量这样的东西具有明显的类型。

我们只需在类型环境中查找变量的类型。

P48:p48 09-07-_Subtyping - 加加zero - BV1Mb42177J7

本视频中,将讨论子类型,面向对象语言中的重要概念。

从let初始化类型规则开始,上次看了let规则,但没有初始化,现在看看添加初始化如何改变,这里会发生什么,首先,注意规则的主体几乎相同,我们在类型为t0的环境中检查e1,类型在let中声明。

其他变量具有o赋予它们的任何类型,我们得到一些类型t1,那将是整个东西的类型,所以这块与之前完全一样,所以新的东西是这行,我们检查初始化器,那么它是如何工作的呢,首先,在假设下,哦,我们检查类型e。

我们得到类型t零,现在这是对主要观点的旁注,但请注意,我们特别使用环境o中的x,新定义的x在e零中不可用,因此,如果e零使用名称x,这意味着它使用名称x的其他定义,该定义在let之外。

因为我们没有包括这个x的定义,在类型检查e零的环境中,现在,但主要观点,我想在这张幻灯片上指出,e0这里为t0类型,与x类型完全相同,是此规则的要求,规定e0必须与x类型相同,实际上相当宽松。

因为实际上没有问题,如果e0的类型是t0的子类型,t0可以包含任何t0的子类型,这绝对没问题,但这里,我们限制自己只允许与x类型完全匹配的初始化器。

若引入类子类型关系,我们能做得更好,最明显的子类型形式是,若x是类,直接继承自y,意味着代码中有x继承自y的语句,则x应是y的子类型,且此关系为传递性,若x是y的子类型,y是z的子类型。

则x也是z的子类型,最后,这也是反射性的,因此每个类都是其自身的子类型,使用子类型,我们可以写出更好的let规则初始化版本,所以再次,主体,规则中处理主体部分,与之前完全一样,所以让我们不要看那个。

现在我们要做的是类型检查e零,我们得到一些类型t零,现在t零只需是t的子类型,所以这里是一条假设,它只是说t零必须是t的子类型,那么t是什么?t现在是x声明的类型,这允许e零具有与x类型不同的类型。

这里唯一的问题是更多的程序,将使用此规则和之前的规则进行检查,之前的规则肯定是正确的,任何使用该规则编译的程序都将正确运行,但这是一个更宽松且仍然正确的规则,更多的程序,呃将编译和类型检查正确。

呃,使用此规则,子类型在许多地方出现在酷类型系统中,这是赋值规则,在很多方面与let规则相似,那么赋值是如何工作的呢?在左侧是变量,右侧是表达式,我们将评估表达式并将返回的任何值,分配给左侧的变量。

那么如何类型检查这个?首先,我们必须在环境中查找x的类型,我们发现它具有某些类型t零,然后我们在相同的环境中类型检查e一,这里的变量集没有变化,因此我们在环境o中类型检查e一,我们得到一些类型t一。

现在什么必须为真,以便此赋值正确?嗯,x必须能够持有类型t一的值,所以x的类型t零必须是t一的超类型,必须大于t一的类型,所以如果此约束得到满足,那么赋值就是正确的。

另一个使用子类型的例子是属性初始化规则,除了标识符的范围外,非常,非常类似于正常赋值规则,所以回忆一下类看起来像什么,},可在cool中声明类,顶部有属性及方法集合,属性定义看起来如何?看起来像这样。

声明变量具有某种类型,右侧可有初始化值,初始化类型在哪检查?在特殊环境o_sub_c中检查,仅包含类c声明的属性类型,因此需遍历类定义,提取所有属性定义,所有变量名及其类型,构建记录这些信息的环境。

然后检查初始化类型,记住属性初始化,可引用类中任何属性,让我们看看如何实现,在环境中查找x类型为t0,在相同环境中检查e1类型为t1,与赋值类似,t1需是,或t0的子类型。

现在看另一个有趣例子,如何检查if then else类型,关于if和else重要的是,类型检查时不知道执行哪分支,不知道程序将执行e1或e2,通常,实际上这个if语句,或这个if表达式在程序运行中。

可能多次执行,有时执行一次,有时执行两次,因此if then else的类型,是e1或e2类型之一,编译时不知道是哪一个,因此最好的做法是类型为。

e1或e2中最大的超类型,计算两个或更多类型上界,经常出现,将操作命名为,lub或x和y的最小上界,x和y的最小上界是z,如果z是上界,意味着它大于x和y,并且是最小的上界,所以这条线说存在另一个z'。

大于x和y,那么z必须小于z',所以z是最小的,是所有可能上界中最小的,在cool和多数面向对象语言中,两个类型的上界就是它们继承树中的最近共同祖先,通常继承树以对象为根,或类似命名的类。

包含程序中所有可能的类,然后有一个层次结构,是一个从对象延伸下来的树,如果我想找到两个类型的上界,比如这个类型和这个类型,我只需要沿着树向上走,直到找到它们的最近共同祖先,所以在这种情况下。

如果我从我树中挑选出这两个类型,这就是这两个类型的上界。

现在我们可以给if-then-else一个类型检查规则,首先要注意的是,if-then-else表达式不会影响环境,if-then-else既不会引入也不会从环境中删除任何变量。

所以所有子表达式都在与整个表达式相同的环境中进行类型检查,现在f和lc零的谓词,应该具有布尔类型,因为那是我们的决定,我们是要走真分支还是假分支,但两个分支可以具有不同的类型,E一只需具有某种类型t一。

E二只需具有某种类型t二,所以再次注意,这说的是什么,这只是在说,E一和E二确实进行了类型检查,它们必须是类型正确的,但我们并不关心类型是什么,类型可以是任何类型,然后整个表达式的类型就是。

t一和t二的上界,因为那将是我们可以给出的,对表达式最终类型的最佳估计,考虑到真分支可能返回t一类型的某些东西,而假分支可能返回t二类型的某些东西。

case表达式的规则是我们迄今为止看到的最复杂的,但实际上它只是一个,嗯,if-then-else的变体,如果我们只是把它拆开,就相对容易理解了,所以让我们先提醒自己case的作用,首先它查看e零。

它评估a零,然后它查看e零的运行时类型,所以它获取e零的动态类,然后它查看第一个分支,它将做什么,它将比较e零在运行时的类型与类型t一,如果t一是e零运行时类型的超类型。

并且实际上它是所有可能分支中最小的,它是所有可能分支中最小的超类型,那么它将选择这个分支,将x1绑定到值,赋予它类型t1,所以将x1绑定到e0的值,将其重新类型化为类型t1,然后评估e1。

所以你可以看到它在什么意义上,是一个华丽的,如果-那么-否则,我们只是在选择最佳匹配的分支,那个声明类型,呃,与e0的运行时类型最接近的分支,然后我们将执行该分支,用在该分支中命名的变量。

绑定到e0的类型,所以让我们看看类型是如何工作的,所以首先我们类型检查e,我们得到一些类型t0,现在会发生什么?如果我们选择第一个分支,那么我们将环境,并扩展它以包含新的变量x1,它将具有类型t1。

所以我们只采取这个分支,记住,如果e0的运行时类型与t1最接近,但如果我们确实采取它,那么我们将执行执行e1在这个环境中,我们将得到一些类型t1 prime的东西,类似地对于所有其他分支,直到最后分支。

这完全与第一个分支相同,只是用字母n替换数字1,并且因为我们不知道哪个分支将在运行时匹配,可能是实际执行的任何一个分支,因此,整个表达式的类型将是所有分支类型,的最小上界。

我在这里只是将最小上界从二元操作扩展到n元操作,这应该足够清楚,我们只是将最小上界取自所有这些类型。

P49:p49 09-08-_Typing_Methods - 加加zero - BV1Mb42177J7

在这视频中,我们将继续讨论类型检查,以及类型检查方法和调用规则。

所以我们要类型检查一个方法调用,假设我们有一个表达式e的派发,我们正在调用名为f的方法,并有一些参数e1到e_n,显然我们要类型检查e_0,它将有一个类型,T_0,同样我们要类型检查所有参数。

它们将有一些类型,然后问题是,此方法调用返回类型?调用此方法后得到何种值?如您可能所见,我们处于与之前非常相似的情况,当我们尝试类型检查变量引用时,我们有一个名为f的名称,但对其功能一无所知。

我们不知道f的行为,除非我们了解s的行为,否则我们无法确定它将返回何种类型的值。

在cool中增加了一个小问题,方法与对象标识符是否分属不同命名空间,在同一作用域内可以有一个名为foo的方法,以及一个名为foo的对象,我们不会将两者混淆,它们在语言中足够不同且使用方式也不同。

因此我们总能分辨,当我们谈论对象foo时,当我们谈论方法foo时,但这意味着,实际上,在Cool中有两个不同的环境,一个用于对象,一个用于方法,因此在类型规则中,这将通过一个单独的映射反映。

一个单独的方法环境,将记录每个方法的签名,这是一个你可能在其他上下文中听到的标准名称,但一个函数的签名只是它的输入和输出类型,因此,这个表m将取一个类的名称,它将取该类中一个方法的名称。

它只是将告诉我们该方法的参数类型是什么,因此,列表中除了最后一个类型之外,都是该方法的参数类型之一,然后,最后一个类型是结果类型,这就是返回值的类型,因此我们编写方法签名的方法就是作为一个元组。

或类型列表,第一个到倒数第二个一起是参数的类型,按顺序,然后最后一个是一个结果的类型,因此在方法环境中像这样的一项,仅仅意味着f有一个看起来像这样签名的,它接受具有相应类型的参数并返回。

某种类型tn加一的返回。

因此,将方法环境添加到我们的规则后,我们现在可以编写一个用于分派的规则,首先注意我们有这两个映射,一个是对象标识符,一个是左转栏上的方法名,我们必须将方法环境传播到所有类型,对于子表达式。

以及方法调用的情况,我们只类型,我们正在调度的表达式的类型e零,以及所有参数和类型t一到tn,然后在类t零中查找f的类型,我们正在向哪个类调度?那将是e零的类,所以我们去哪里查找,M在我们的环境中。

最好有一个名为f的方法在类t零中找到,它必须具有正确的参数数量,然后我们正在传递的实际参数e一到e n,它们的类型必须是声明的形式参数的子类型,所以这里f的签名说,例如,f的第一个参数类型为t一撇。

因此我们将要求e一的类型为某种类型t一,使得t一是t一撇的子类型,类似地,对于方法调用的所有其他参数,如果所有这些检查都通过,如果f有这样的签名,并且实际参数和形式参数的子类型要求匹配。

那么我们将说整个表达式,此调度返回类型tn加一。

方法的返回类型,静态调度的类型规则与常规调度的类型规则非常相似,所以回忆一下语法上,唯一的区别是程序员写了类的名称,他们希望在运行方法,所以不是运行方法f,如e零的类定义,无论那个类是什么。

我们将运行f在e零的类的某个祖先类中,类中,那如何在类型规则中表达,好吧,我们再次类型e零和所有参数,现在我们需要的是我们为e零发现的任何类型,它必须是t的子类型。

所以t必须是e零类型层次结构中的祖先类型,此外,类t最好有一个名为f的方法,具有正确的参数数量,具有正确的类型,以便所有类型约束都能解决,实际参数类型是相应形式参数类型的子类型。

然后如果所有这些都是真的,我们将能够得出结论,整个调度表达式具有类型tn,加一。

方法的返回类型是什么,方法环境必须添加到系统中的所有类型规则中,嗯,这很容易做到,因为只有调度规则真正关心方法是什么,其余的规则只是将方法环境传递给其他规则,那我的意思是什么,嗯,这是我们的加法规则。

只有对象环境,现在我们要做的是添加一个方法环境,子表达式将使用与整个表达式完全相同的方法环境进行类型检查,其余的规则只是从根向叶传递方法环境,而不改变它。

就像这个规则一样,对于涉及self类型的某些情况,实际上我们需要环境中的更多东西,因此,cool类型检查的实际完整环境由三部分组成,首先,有一个映射o,它将类型分配给对象ID,有一个映射m。

它将类型分配给方法,最后,我们只需要知道当前类的名称,因此,我们正在类型检查的表达式所在的类。

因此,cool类型检查逻辑中的句子完整形式如下,并读作如下,在假设对象标识符具有由o给出的类型的情况下,方法具有由m给出的签名,并且表达式s位于类c中,那么我们可以证明表达式e具有类型t,这是一个例子。

加法示例,再次写出带有完整环境的加法规则。

我给你的类型检查规则很酷,具体,其他语言有不同的规则,但类型检查有一些普遍的主题,首先,类型规则定义在表达式的结构上,因此它们通常以这种归纳方式完成,其中表达式的类型。

表达式的类型取决于其子表达式的类型,以及变量的类型,和,更一般地说,表达式中的任何自由名称,如方法名,它们将由环境建模,因此我们将有一些映射围绕,它告诉我们对于表达式中的任何类型的自由名称。

类型规则应该对,这些名称的类型做出什么假设,你可能已经注意到的一件事,但值得明确的是,类型规则非常紧凑,符号并不复杂,这些角色中有很多信息,需要花时间仔细阅读。

P5:p05 02-02-_Cool_Example_II - 加加zero - BV1Mb42177J7

欢迎回到本视频,我们将看另一个酷编程示例。

这次让我们超越简单的Hello World,转向更令人兴奋的东西。

比如流行的阶乘函数,因此为了编写阶乘,我们需要打开一个可以编写代码的文件,让我开始并回忆上次,每个酷程序都需要有一个主类,主类需要有一个main方法,我们不在乎main方法返回什么,所以我们将有它。

返回某种类型的对象,让我在这里填充文件的骨架,所以现在我们可以编写一些代码,所以main方法将做什么,在我们实际编写阶乘之前,在我们深入这个程序之前,实际上并不难,我们需要更多地谈论io。

因为我们需要能够读写数字,我们将能够从运行程序的用户那里读取数字,并打印它们,所以让我们回顾一下io的一些内容,为了调用io函数,我们需要一个io对象,其中一个io函数是打印字符串的。

所以让我们编写一个我们已经知道如何做的程序,只是为了确认我们记得,现在我们可以编译这个程序,它应该只打印一,让我们看看确实如此,它做到了,好的,所以它打印出数字一,所以现在,嗯,让我们回到这里。

谈谈如何做输入,所以不是只打印数字一,让我们打印出用户输入的字符串,所以在这里我们将读取一个字符串,为了做到这一点,我们需要一个io对象,因为还有一个函数,另一个方法叫做in_string,好的。

这将读取一个字符串,然后嗯,返回一个字符串,然后为了确保我们得到漂亮的输出,让我们将这个字符串连接到一个新行,以确保我们得到漂亮的输出,这就是为了,当它打印字符串时,它将另起一行打印。

所以让我们尝试编译这个,编译成功,现在我们可以运行spin,记住Unix中的感叹号命令会运行以相同字母开头的上一个命令,现在程序运行了,它在等待,因为它在等我输入,如果我输入1,它返回1。

如果我输入42。

它返回42,好的,现在我们需要讨论的是如何将字符串转换为整数,因为如果我们做阶乘,我们想处理整数而不是字符串,目前我们只是在读写字符串,所以Cool中有一个库用于整数和字符串之间的转换。

我们将给这个主类,赋予那个类的功能,嗯,它被称为a2i,I代表ascii到整数,它定义了一组可以将字符串和整数相互转换的方法,所以让我们在这里添加这些命令,这是我们的字符串,我们读入的。

现在我们要将其转换为整数,让我在这里添加几个括号,这是我们的字符串,好的,现在我们要调用那个方法,抱歉,我们要调用那个函数,那个方法,a2i,让我们再检查一下,确保参数在正确的位置,这是a2i的参数。

现在我想起当我们有一个方法分发,它单独存在,没有对象,它是对self对象的分发,self对象是当前类中的对象,在这种情况下是main对象,它继承了a2i方法,因此a2i函数应该在那里定义。

现在我们有了一个整数,我们可以用那个整数做点什么,如果我们喜欢,让我们再加些括号,假设我们只给整数加1,好的,然后我们处理完整数,无论我们想对整数做什么操作,我们需要将其转换回字符串。

这样我们才能打印出来,有一个逆函数,I到a的函数会这样做,我不知道此时括号是否都放在正确的位置,那就确认是,看起来可行,嗯,这将读取字符串,转换,转换为整数,加1,嗯,转换回字符串,拼接新行并输出。

看看是否都管用,运行编译器,有问题,啊,说我们有个未定义类a到i。

原因是没提供a2的代码,所以看目录,已复制a2的类文件,鼓励你去看代码,很有趣,看如何在Cool中写转换,现在谈如何编译用库的程序,你这样做的方式,非常简单,编译时,只需在命令行列出所有类文件。

它会读取所有文件并视为单个程序,在这种情况下,我们编译compile fact和a two。

编译完成。

然后可以运行,现在如果我输入3。

输出4,如果我输入1。

打印两个,程序似乎运行正常,现在我们可以编写阶乘函数了,阶乘中我们想做什么,嗯,我们不想只是加一,相反,我们想调用特殊函数阶乘,让我们在这里插入阶乘的调用,好的,去掉加1,然后检查所需括号。

关闭a的阶乘,调用i至a的调用,最后一个是输出字符串调用,没问题,现在可以添加fact方法,实际上,它将接受整数参数,这里需要参数,类型为int,当然,整个将返回一个整数,然后,嗯,我们需要函数的主体。

可能是个好主意,只是为了确保我们做得对,做一些简单的事,所以让我们试着做一个返回其参数加一的函数,这将做与之前完全相同的事情,让我们确认那是工作的。

所以我们用两个i库编译,现在我们有语法错误,我们发现我忘了方法后的分号,记住,嗯,类体是一个方法列表,每个方法以分号结束。

让我们再次编译,现在编译成功。

让我们运行它,我们输入4,返回5,没问题,现在可以写阶乘代码了,这可能会很平淡,因为代码实际上很简单,如果我们递归编写,那么让我们这样做,那么这将如何工作呢,每个人都记得定义,我希望如果i等于0。

那么0的阶乘是1,我们有一个关键字,然后一个,否则,嗯,阶乘将是,嗯,我乘以i减一的阶乘,对吧,然后if语句很酷,嗯,总是以关键字fee结束,所以它是一个if-then-else-fee结构。

现在应有一个计算阶乘的程序,编译成功,现在运行它。

3的阶乘是6。

6的阶乘是720,看起来正确,再试一次,用个大数,得到一个,得到一个很大的数,我们认为可能是正确的,总之,阶乘函数,工作正常,现在回到这里,作为练习,嗯,重写这段代码为迭代,不用递归函数,用循环写。

为此,去掉这段代码,需要什么,嗯,需要一个累加器,需要一个,局部变量来累加阶乘计算结果,在Cool中声明局部变量用let语句或let表达式,所以有,叫fact的变量表示阶乘结果。

注意这里变量名可与函数名相同,编程语言Cool不会混淆,因为变量和函数扮演不同角色,阶乘fact,抱歉,类型为int,初始化为1,好,这样乘法,会工作,嗯,整数默认初始化为0。

如果我们乘fact和其他数,就不好了,好,let有两部分,声明的变量或变量列表,实际上可以是一个变量列表,这次只有一个,然后是主体,fact变量可用的表达式或计算,我们想做什么,嗯,所以我认为我们需要。

这是一个语句块,我们需要不止一个连续的语句,我们马上就会明白为什么,但接下来我们想要一个循环,那么我们的循环会做什么呢,我们会说,当i不等于0时,我们要做什么,我们需要做什么,嗯,循环体的开头。

开头的关键字叫做loop,现在我认为我们要进入另一个语句块,所以让我们打开一个块,我们可能需要做不止一件事,第一件事是我们想要有fact,是fact乘以i,好的,我们知道i不是0。

所以我们需要将i的当前值乘以fact来累积结果,然后我们想要从i中减去1,注意在cool中,赋值语句是这样的箭头,这就是你如何做赋值,它也是你如何做初始化的,初始化和赋值,看起来一样。

然后我们可以关闭语句块,好的,while循环的主体总是单个表达式,在这种情况下,这个表达式是一个块,由两个语句组成,然后我们可以关闭循环,循环的关闭是pool关键字,呃,然后呃。

现在我们处于一个语句块中,所以这个必须以分号结束,上面的语句块从let开始,现在我们需要let块的结果,或者是let表达式的结果是阶乘,所以无论我们从while循环中得到什么。

无论我们在while循环中计算了什么,我们希望那成为整个let表达式的结果,这是块中的最后一个语句,记住,块中的最后一个语句,是块的值,let的主体是let的结果。

所以fact也将是整个let语句的结果,它只是语句块的值,由于阶乘方法的主体本身只是一个let表达式,fact将是整个东西的结果,所以如果我们写对了,未犯任何错误。

应为阶乘的迭代版本,所以让我们编译这个。

令人惊讶的是第一次编译成功,现在让我们运行它。

哇,它实际上起作用了,所以我们得到了六,让我们再做一次测试,看看它是否承诺我们。

事情进展顺利,现在让我指出一个常见的错误,你可以很容易地犯,也是我犯的错误,当我有一段时间没有编写酷程序时,如果你是C或程序员或Java程序员,你可能会考虑像这样编写赋值。

所以我只是使用等号来编写看起来完全正确的赋值,如果你熟悉这些语言或习惯于在这些语言中编程。

现在让我们看看当我们尝试编译时会发生什么,这个编译得很好。

然后当我们尝试运行它时会发生什么。

哦,它运行了,对于一个输入,所以让我们给它一个输入,然后我们看到我们耗尽了热量,看起来像一个无限循环,所以我们一直在循环中转圈,不知何故消耗着内存,我们将在课程稍后讨论为什么这个循环实际上会消耗内存。

但显然我们在循环中没有足够的内存,最终我们耗尽,所以那是一个无限循环的明确标志,所以这里发生了什么,嗯,问题是,在酷中,等于等于运算符是比较运算符,所以在这里,如你所知,我们比较了i是否为零。

那返回一个布尔值,所以这些都是完全有效的酷表达式,它们只是碰巧是布尔值,所以你实际上并没有在这个程序中更新i或阶乘,你只是在比较fact与fact乘以i,以及i与i减一,程序很高兴这样做,嗯。

它只是没有计算阶乘函数,永远不会终止,因为i从未达到零,所以这结束了我们的阶乘示例,我们下次再做另一个更复杂的酷程序示例,带有一些非平凡的数据结构,这就是全部。

P50:p50 10-01-_Static_vs._Dynam - 加加zero - BV1Mb42177J7

本视频中,将讨论静态类型与动态类型。

一种考虑类型系统目的美学方法是防止常见编程错误,它们在编译时做到这一点,因此它们在程序编译时做到这一点,特别是它们在不了解程序任何输入的情况下做到这一点,因此唯一可用的就是程序文本。

这就是为什么我们称它们为静态的,因为它们不涉及任何动态行为。

程序的实际执行行为,现在,任何正确类型系统,任何真正做对事的静态类型系统,将不得不禁止一些正确程序,它不能在编译时完全精确地推理,关于程序运行时可能发生的一切,这意味着一些正确程序,我指的是。

一些实际上会正确运行的程序,如果执行它们将被类型检查器禁止,有人主张动态类型检查,这是程序运行时进行的类型检查,因此,在运行时,我们检查实际操作是否合适,对于程序执行时出现的数据,其他人说,问题在于。

类型系统不够表达力,我们应该开发更复杂的静态类型检查系统,随着时间的推移,两个阵营都有了很大发展,我们看到了许多新的动态类型检查语言出现,现代脚本语言,如语言和领域特定语言,其他人。

我一直在研究更复杂的类型系统,实际上静态检查方面已经取得了很大进展,更表达丰富的静态类型检查系统的缺点是,它们确实倾向于变得更复杂,不过,并非所有人开发的功能都已进入主流语言。

现在讨论中一个重要观点是存在两种不同的类型概念,有动态类型,那是类型,我们谈论的对象或值在运行时实际具有的类型。

然后是静态类型,编译时概念,类型检查器知道关于对象的信息,静态类型和动态类型之间必须存在某种关系,如果静态类型检查器要正确,这种关系可以通过某种定理正式化,证明如下,我们想了解的是对于每个表达式e。

对于你能在编程语言中编写的每个程序表达式e,静态类型,编译器说表达式的类型,等于表达式的动态类型,另一种说法是,如果你实际运行程序,那么得到的结果与静态类型检查器预期的结果一致。

静态类型检查器实际上能够正确预测,运行时将出现的值,实际上,在编程语言的早期,这些正是我们对语言中简单类型系统的定理。

那时,现在对于像Cool这样的语言,情况有点复杂,让我们看看一个典型Cool程序的执行,这里有两个类,类A和继承自A的类B,所以B是A的子类型,我们这样写,这里声明了x的类型为A,这是x的静态类型。

x的静态类型是A,这是编译器对x的值所知道的信息,然后当我们执行这行代码时,我们可以看到我们给x分配了一个新的A对象,它是新的并不重要,重要的是,它是一个A对象,所以在这个点上x的动态类型也是A。

这行代码实际上执行时,A,被声明为具有静态类型A,实际上包含一个A类对象,但在这行代码稍后,动态类型实际上不同,这里的x的动态类型将是B,好的,当这行代码执行时x持有B对象,尽管它被声明为具有不同类型。

这是一个非常重要的区别要记住,所以有一个静态类型,编译器知道的一种类型,它是恒定的x的类型是A,它总是类型A,x的所有使用,在整个x的作用域内,都由编译器用类A类型,但在运行时,因为我们有赋值。

我们可以给x分配不同的对象,x实际上可以具有不同类型和不同的运行时类型,这是一个类型为A的对象,这是一个被分配给x的类或类型B的对象,当程序执行时。

这意味着,Cool类型系统的正确性定理,比简单类型系统的定理更复杂,在子类型存在的情况下,尽管,我们想要的是属性,对于一个给定的表达式,E将是一个正确的预测器,所有可能的动态类型,他可能会拥有。

通过使用子类型关系,所以无论动态类型e可能是什么。

运行时可以采取的所有类型,还须能用于任何c'对象,c的子类,定义了属性方法,c'中所有属性和方法都要有,因此子类只能添加属性方法,所以子类将有的属性方法与c、c'相同,在这种情况下,这些都是。

除了c已有的,c'还有,子类不会移除属性或方法,只会扩展或添加继承类的属性和方法,注意,你可以在酷炫且大多数面向对象语言中重定义方法,但你不能改变类型,即使你可以重定义与该方法相关的代码。

它仍需根据你声明的原始类型进行类型检查,因此,方法在首次定义的类中具有的任何类型,它将在所有子类中具有相同的类型,相同的参数,以及方法参数的相同类型,和方法结果的相同类型在所有子类中。

这是许多面向对象语言的一个相当标准的设点。

P51:p51 10-02-_Self_Type - 加加zero - BV1Mb42177J7

在上个视频中,我们讨论了静态和动态类型之间的区别,以及静态类型中越来越具表现力的类型系统的趋势,在这节课中,我们将讨论自类型,这将让你感受到那些更具表现力的类型系统的样子。

首先,让我们通过查看一个简单的类定义来激励自类型解决的问题,所以这里我们有一个计数类,它有一个字段,I,它是一个整数,初始化为零,它有一个increment方法,本质上,计数类只是增加了一个计数器。

所以当你分配一个新的计数对象时,计数器为零,然后每次你调用ink,计数器值增加一,请注意,这可以被视为一个提供计数功能的基类,所以每当我想为特定目的使用计数器时,我可以定义一个新的子类,继承自计数。

该子类将自动继承ink方法,从而允许我拥有计数器而不必重新实现代码,在这种情况下,代码量非常,非常小,但通常,你可以想象有一个实现了一些复杂或需要大量代码的类,能够重用它在子类中是有用的。

现在考虑一个我们可能想要定义的计数子类,叫做库存,假设我们正在实现一个仓库会计程序,并且我们想跟踪不同种类的库存物品的数量,所以我们定义一个新的类,库存继承自计数。

现在我们将有一个新的字段在这里使这个对象,这个类不同于它的父类,将只有一个名称,对应库存中的物品名称,现在在这里我们可以实际使用这个,我们可以声明,分配一个新的库存对象,我们创建一个新的对象。

我们增加它来表示我们有库存中的一件东西,然后将其分配给我们声明的库存类型的某个变量,然后稍后我们可以像使用a对象一样使用它,但现在的问题是,这段代码实际上不会类型检查,这段代码中有类型错误。

为什么是这样呢?让我们思考一分钟,所以ink的签名是什么?所以记得ink被声明为返回计数类型的东西,当ink方法被库存类继承时,这个签名不会改变,它仍然返回计数类型的东西,这里有一个新的股票对象。

我们调用增量方法,但整个东西类型是计数,然后尝试将其分配给股票,但那不起作用,因为计数不是股票的子类型,股票类型的变量不能持有计数类型的值,因此类型系统将在赋值语句处报告错误。

你可以看到这实际上是一个严重问题,因为它使增量方法的继承变得相当无用,我可以定义新的股票子类,但我永远无法在它们上使用增量方法,至少在不得到父类型返回值的情况下,所以它不是。

增量方法的继承不如人们所希望的那么有用。

所以只是回顾一下,新的股票,增量后的新股票,动态类型为股票,实际上将返回一个股票对象,好的,所以不要在这里混淆,这是动态类型,我在谈论,当我们分配新的股票对象时,然后我们调用增量方法。

记住增量方法返回self,所以增量方法实现如下,我将省略类型,但它是i得到i加1,然后它返回了self对象,所以它肯定返回传递给这里的任何对象,所以它返回动态类型为股票的东西,所以这个程序实际上会运行。

如果你没有类型检查,你可以实际运行这个,它将会正常工作,这将产生一个动态股票对象,并将它存储到股票变量中,但它类型不正确,因为类型检查器失去了跟踪这是一个股票对象的事实。

它只知道增量被声明为返回类型计数,这当然正确,因为每个股票对象也是一个计数对象,但在这段代码的上下文中它并不实用,因此类型检查器丢失了信息,这使得尝试从一开始就将增量方法放入账户类中并不愉快。

为了解决这个问题。

我们将研究扩展类型系统,我们将研究扩展类型系统,见解是增量方法返回self,在这种情况下,增量方法实际返回self对象,因此返回值将具有与self相同的类型,无论self是什么,可能是计数。

也可能是计数的任何子类型,因此,self对象只需动态地持有某个值,它是self参数声明的类型的子类型,因此它可以是任何子类型。

在这种count类的情形下,为此,实际上需要引入一个新关键字:self类型,它将用于作为此类函数返回值的类型返回,我们需要修改类型规则。

以处理这种新型类型,self类型的想法是它将允许类型改变,当继承ink时,或允许我们推理实际返回类型如何动态变化,当继承增量方法时,因此我们更改了ink的声明如下,现在声明返回类型为自身类型。

意味着增量方法的返回值具有,原始self参数的类型,当我们这样做时,现在我们可以看到这是可能的,我们没有,我们还没有说如何做到这一点,但你应该能够看到直观上是有意义的,我们可以证明以下形式的事实,因此。

当self参数具有类型计数,记住我们派发的是,但称为墨迹的是self参数,所以当我们派发到计数对象,我们得到类型为count的东西,当我们派发到股票对象时,当我们调用股票对象的increment,嗯。

self的类型是什么,类型是股票,我们得到类型为股票的东西,现在,我们之前的这个程序加上这个改变,类型正确,将被酷的类型系统接受。

现在,记住self类型不是一个动态类型非常重要,它非常像是一个静态类型,是静态类型系统的一部分,同样重要的是要认识到self类型不是一个类名,所以,与酷的所有其他静态类型不同,它不是类的名字。

它是自己特殊的东西,我们将在未来的视频中更多地谈论它到底是什么,如我们所见,使类型检查器接受更多正确程序。

实质上,self类型增强了类型系统的表达能力。

P52:p52 10-03-_Self_Type_Operat - 加加zero - BV1Mb42177J7

本视频中,我们将继续讨论self类型,通过讨论self类型的操作,这将有助于澄清self类型是什么。

及其在类型系统中的作用,所以让我们从思考上次讨论的例子开始,如果你忘了那是什么,让我快速写下来,我们有一个名为count的类,count有一个字段,一个初始化为零的整数i,它有一个名为ink的方法。

返回类型为self类型的某个东西,所有ink所做的就是增加计数器字段,并返回self对象,我可能犯了一些语法错误,但那并不重要,这是count类的基本代码,问题是ink实际返回的对象的动态类型是什么。

答案是它可以是self对象的动态类型,self对象的动态类型,如果我们考虑一个大程序,其中多个类继承自count,那么答案是ink可以返回,count或count的任何子类,所以它将返回至少。

count。

但它可以返回更具体的类型,动态类型可以是更具体的,它可以是count的子类,或count子类的子类,任何直接或间接继承自count的类都是可能的,那么一般情况是什么,让我们考虑一个类c,在这个类c中。

有一些表达式内部具有self类型,这个表达式如何获得self类型并不重要,我们只需说它具有这种类型,以某种方式,那么表达式的可能动态类型是什么,根据我们上一页的讨论,显然表达式的动态类型,当你运行e时。

你将得到c类的子类型,包含self类型的类,这很有趣,因为它向我们展示了self类型的含义实际上取决于上下文,所以self类型意味着,c类的子类型,如果我在类d中写了self类型。

在类d的定义的某个地方,将self类型实例替换为类名,So self类型c,这里将指向关键词的一个语法实例,类c中的self类型,这还暗示了一个非常简单的类型角色,关于self类型第一个有用的事实是。

即self类型sub c是c的子类型,这确实是一个关键的想法,一个self类型的类c是c的某个子类型,因为这也有助于说明self类型,真正最好的思考方式是self类型,是一个类型变量。

其范围涵盖所有子类,它出现的类,所以self类型sub c你应该认为是一个类型变量,它没有固定的类型,但保证是某种类型,受限于c,所以它将是直接继承。

或间接从类c继承的类之一,self类型c是c的子类型的规则有一个重要的后果,这意味着当我们使用self类型进行类型检查时,总是安全的,总是安全地将self类型sub c替换为c。

所以它是安全的提升任何self类型c,它可能是c或c的子类型,就说好吧,我们只是说它是c就对了,这暗示了一种处理self类型的方式,即用c替换所有self类型sub c的出现,不幸的是。

那最终并不是很有用,它是正确的,这样做是正确的,但那真的就像根本没有self类型一样,就像我们回到上一视频中做的例子,我们开始时没有self类型,我们发现我们不能按预期使用继承。

所以为了做得比仅仅扔掉所有self类型更好,我们需要将self类型纳入类型系统,我们将要做的,是通过查看类型系统中的操作来做到这一点,有两个操作,我们之前讨论过的子类型关系。

所以当一个类型是另一个类型的子类型时,以及告诉我们两个参数类型中较小类型的最小上界操作,它比两个参数类型都大,我们所有要做的就是我们现在要做的。

是扩展这些操作以处理类型self type,所以让我们从子类型关系开始,在我们的定义中,我们将使用一些类型t和t prime,它们只是正常的类名,它们是任何类名,但不是self类型。

所以一种可能是我们在子类型关系的两边都有self类型,在这种情况下,显然,self类型子类c应是self类型的子类,子类c,为了说服自己这一点,再次将self类型视为变量,我们可以为该变量插入。

c的任何子类型,但就像代数中的变量一样,如果我们为一个变量的出现插入一个特定类,对于该变量的每个出现,我们都必须选择相同的类,特别是,如果我们选择c的某个子类a,那么我们最终会得到a是a的子类。

如果我们为两边插入a,我们可以看到这种关系同样成立,c是c的子类,对于我们可能选择的任何其他子类型,如果我们将这个变量绑定到该子类型,我们现在可以看到这种关系将是真实的,你可能还会想,嗯。

如果self类型子类c与另一个类的self类型进行比较,比如d的self类型,结果发现,在酷类型规则中,这永远不会发生,酷类型规则被编写成,我们永远不需要比较来自不同类的self类型。

我还没有向你展示这一点,但当我们实际遍历self类型的类型规则时,你会看到这是真的,现在另一种可能是我们有一边的self类型,而另一边是常规类型,那么,何时self类型子类c是t的子类型?嗯,我们将说。

如果这是真的,那么c是t的子类型,我们在这里使用我们的规则,总是安全地将self类型替换为其索引的类,在这种情况下,由于c是任何self类型子类c可能是的超类,如果c是t的子类型。

如果t至少是c或更高层次类层次中的某些东西,那么t将是任何self类型子类c可能代表的超类。

另一种情况是,当我们在子类型关系的一边有常规类名时,而self类型在右边,在这种情况下,我们发现我们必须说这种关系是错误的,即t是常规类名,永远不会是self类型子类c的子类型,要看到这一点。

只需考虑可能性,所以c和t可能在类型层次结构中的位置,所以如果t和c无关,你知道,如果它们继承自对象,且彼此无关,那么显然t不能是self类型sub c的子类型,它们只是两个无关的类。

所以唯一可能奏效的方式是,如果它们以某种方式相关,如果t是c的子类型,那么你可能认为这可以奏效,但结果是我们不能允许它,即使在那种情况下,这是原因,考虑一个t有子类的层次结构,假设它有一个子类a,现在。

因为self类型sub c遍历c的所有可能子类型,我们可以将a插入这里,t不是a的子类型,它们处于错误的关系中,因此,因为这对b c的所有可能子类型都不奏效,我们不能说这是真的,我们必须说它是假的。

现在有一个非常特殊的情况,有人可能会认为我们应该允许这是真的,那就是在t实际上是类层次结构的叶节点的情况下,让我实际上以不同的方式绘制这个,以强调这一点,假设c是一个上面的类,然后t,你知道。

通过一些继承关系是c的子类型,它不是直接的,但可能还有其他类在中间,只是强调这不是不,这种关系不必是直接继承,它可以是传递继承,现在如果t是层次结构的叶节点,并且它是c的唯一叶节点,如果c没有其他子类。

那么实际上t是self类型sub c的子类型,因为它是c子类型层次结构中唯一的极小类型,但问题是这非常脆弱,不起作用,如果你修改程序,特别是如果程序员过来添加一个与t无关的类a,但也是c的子类。

那么这将不再奏效,因为如果我将a插入self类型sub c中,那么我看到t不是a的子类型,所以我们可以在非常特殊的情况下允许它,即c只有继承链,而不是下面的通用树,并且t是该链的叶节点。

但这是对未来程序扩展非常脆弱的,我们知道,如果你通过在这里添加另一个类打破了它,突然之间,你会在之前经过类型检查并工作的代码片段中收到类型错误,而且这些代码片段根本没有改变。

它就不会是一个很好的语言设计,所以,我们不会允许它在非常特殊的情况下,即c只有继承链,总结:t不是self类型的子类型,最后,嗯,如果我们比较两个非self类型的正常类型,那么我们就使用之前给出的规则。

对于正常类名的子类型规则没有改变,这涵盖了所有四种情况,两边都可以有self类型,self类型可以只在左边或只在右边。

最后,我们可以有一个没有self类型的子类型关系,现在让我们继续讨论上确界操作,tnt prime可以是任何类型,除了self类型,self类型的上确界就是self类型,我认为这很清楚。

self类型的上确界,Sub c和t将是类c和t的上确界,这是因为c是self类型可能成为的最大类型,因此,最大的类型确保覆盖,c和t的self类型将是c和t的上确界,上确界是一个对称操作。

所以如果我反转这两个参数,答案是一样的,最后,如果self类型不是上确界参数之一,那么我们之前做过的就做之前的事,上确界定义,对不起,没有改变类名的上确界。

P53:p53 10-04-_Self_Type_Usage - 加加zero - BV1Mb42177J7

本视频介绍了自类型操作,接下来将讨论自类型在酷炫中的应用。

解析器检查自类型仅在允许类型的地方出现,但实际上这有点过于宽松,有些地方可以出现其他类型,但自类型不行,因此,本视频的目的就是讲解自类型的各种使用规则,让我们从一条非常简单的规则开始。

自类型不是一个类名,因此不能出现在类定义中,既不能是类的名称,也不能是继承的类,在属性声明中,属性的类型,t为自类型是可以的,因此,声明为自类型的属性是可以的。

类类型的属性,同样,自类型的局部let绑定变量是可以的,分配新的自类型对象是可以的,并且,实际上,它会分配一个具有与self对象相同动态类型的对象,因此,self对象的类型是什么。

运行时不一定与包含类的类型相同,新的t操作将创建该动态类型的新对象,美学调度中命名的类型不能是自类型,因为它必须是一个实际的类名。

最后让我们考虑方法定义,这是一个非常简单的方法定义,有一个形式参数x,类型为t,方法返回一些类型为t prime的东西,现在发现只有t prime,只有返回类型可以是自类型,没有参数类型可以是自类型。

让我们以两种不同的方式看看为什么,为什么这必须是这种情况,我们都会做,因为这实际上很重要,让我们考虑对这个方法的调度,假设我们有一些表达式e,并调用方法m,我们有一些参数e prime。

假设参数e prime的类型为t zero,如果你还记得方法调用的规则,t zero必须是要传递的类型的子类型,我们将传递这个,因此,无论x被声明为什么类型,这里必须是实际参数类型的超类型。

这意味着t zero必须是,现在假设参数可以是自类型,那么t zero必须要是自类型的子类型,这是类c中的某个方法,记住我们说过这是始终为假,你不能在右边有self类型,左边有常规类型。

因为这会导致问题,这会,我们永远无法证明,一般来说,一个类型实际上是self类型的子类型,因为self类型可以遍历类c的所有子类型,所以这是一种看法,我们不能允许方法参数为self类型。

但仅仅考虑执行代码或一些示例代码也很有帮助,看看会发生什么,所以这是一个例子,让我带你走过,如果我们允许参数具有self类型会发生什么,有两个类定义,类a有一个比较方法comp。

它接受一个self类型的参数,并返回一个布尔值,这里的想法是,比较操作可能比较这个参数与参数,并返回真或假,然后有一个第二个类b,b是a的子类型,继承自a,它有一个新的字段b,一个小b这里。

类型为int,现在类b中的比较函数被覆盖,它与类a中的比较函数或comp函数具有相同的签名,但方法体这里访问了字段b,现在让我们看看,使用这两个类的代码会发生什么,所以这里x将被声明为类型a。

但我们将分配给它一些类型为b的东西,这里注意静态类型将是a,动态类型将是b,这实际上是问题的关键,现在我们在x上调用cup方法,并传递给它一个新的a对象,那么会发生什么,类型检查是好的,因为x属于类a。

x的类型是a,这个参数的类型也是a,如果self类型,如果有一个参数类型为,Self类型是有用的,它必须工作,对于这个例子,其中两个静态类型,调用的参数和形式参数的,显然必须允许。

如果我们允许self类型作为参数的类型,现在让我们想想当它实际执行时会发生什么,将调用方法,b类的comp方法,好的,因为x是动态类型b,然后它将接收参数,并将访问其b字段。

但参数是动态类型a且无b字段,实际上这将导致运行时崩溃,所以再回顾一次,确保这里清楚,过剩类型a,但动态类型b,参数静态类型a,动态类型a,当此方法被调用时,动态类型a的参数,没有操作。

类b的所有字段和方法。

P54:p54 10-05-_Self_Type_Checki - 加加zero - BV1Mb42177J7

本视频中,将用所学自类型,将自类型融入cool类型检查规则。

首先回顾cool类型检查规则实际证明,类型逻辑中的句子像这样,它们证明某些表达式具有某些类型,在对象标识符具有由o给出的类型假设下,方法具有由m给出的签名和包含类,e所在的当前类。

我们在其中进行类型检查是类c,此额外部分的原因,我们之前没讨论过,为何需要这个c,因为self类型含义取决于包含类,回忆下我们引入self类型子c表示,特定self类型s出现,环境中的c正是那个下标。

跟踪我们在哪个类,所以看到self类型出现。

我们知道谈论哪种self类型,现在准备好给出使用self类型的类型规则,大体上,这很简单,因为规则保持不变,它们看起来一样,但实际上略有不同,因为它们使用了之前定义的新子类型和最小上界操作,例如。

这是赋值规则,这与几周前讨论的赋值规则看起来相同,但注意这种子类型使用,这是包含自类型的子类型扩展定义,现在这条规则也适用于self类型和普通类名。

存在一些规则需要在self类型出现时更改,特别是分发规则需要更新,这是动态分派的旧规则,这条规则这部分实际上没有改变,保持不变,但我想指出,这条规则中的基本限制,是方法返回类型不能是self类型。

实际上这是self类型为我们带来好处的所在,拥有自类型目的在于,实现更丰富的表达能力。

现在需考虑情况,已有自类型和所做工作,若方法返回自类型,如何类型检查,规则如下,通常检查分发表达式,即e0和所有参数,它们在相同环境中类型检查,现在和之前一样查找类t0,表达式e的类型,零,方法f。

获取其签名,然后必须检查参数是否一致,每个实际参数,E1到E n,其类型与方法签名中的相应形式参数兼容,如果这些都成功,那么可以说,此分发将具有类型,哦,看零,那么那来自哪里呢,返回类型是自身类型。

因此,此整个分发的结果将是,e零的类型,e零是自参数,无论我们为e零得到了什么类型,这都是整个表达式的有效静态类型,整个表达式的结果类型,我们仅用e零类型作为整体类型。

嗯,静态作为整体动态分发,回忆函数的正式参数不能有self类型,但实际参数可以有self类型,扩展子类型关系将处理该情况,非常好,一个有趣细节是分发表达式本身可以有self类型,我指什么?让我们想想。

呃,零和呃,分发到方法定义,如果e零是self类型,如果我们能证明e零是self类型,问题是需要在m环境中查找,在方法环境中,在某个类中,方法f的定义或签名,我们必须获取该类型签名。

以便我们可以进行其余的类型检查,若e零为自身类型,通常用e零类型查找,这里用哪种类型?若整个操作在类c中,若我们有,若在类c中类型检查,我就把行放那,这样安全,这是self类型子c,和往常一样。

替换子类c的自类型为c是安全的,因此我们只需使用类c,当前正在类型检查的类以查找方法名f。

对于静态分发,我们也要做类似的变化,这是静态分发的原始规则,这部分规则不会改变,嗯,这处理方法返回类型不是自类型的情形。

但如果方法返回类型是自类型,那么规则看起来有点不同,所以,嗯,我们再次类型检查,检查要分派的表达式,以及整个表达式环境中所有参数,但我们必须检查要分派的类,类型t0是类,在静态分发中命名的类的子类型。

嗯,我们需要查找方法,它必须在静态分发的类中存在,因此我们必须在类t中查找,方法f并获取其签名,然后我们必须检查实际参数是否符合形式参数的类型,参数类型匹配,类型,形式参数声明的类型。

然后关于这个规则有点奇怪的地方,结果是t0再次,为什么这是正确的。

它可以是t,它可以是我们静态分派的类型,这不是因为自类型是self参数的类型,即使我们正在类t中分派到一个方法,self参数仍然具有类型t0,我们称t0是t的子类型。

因此我们使用静态分发来达到一个方法定义,可能被子类中覆盖的方法隐藏,但这不会改变self参数的类型,self参数仍然具有类型t0,即使我们正在运行t0的超类中的方法,有两个关于自类型的新规则。

一个涉及self对象,因此self对象具有类型self types of c,注意这是我们需要知道包含类的地方,因此我们知道我们指的是哪种自类型,类似地,对于分配某种类型的自类型也有规则。

因此表达式new self type也产生具有类型self type sub c的东西,总结一下这段视频,这里有一些关于在自类型存在的情况下实现类型检查的评论,结束,首先。

扩展子类型和最小上界操作能做很多工作,如果你以我们做的方式扩展子类型和最小上界,那么很多规则就不必改变,大部分情况下,你不需要为自类型做任何特殊处理,自类型只能在语言中很少的几个地方使用。

由你来检查它没有在其他任何地方使用,这些限制必须仔细遵守。

最后,大部分情况下,自类型的使用总是指当前类的任何子类型,类型检查中有一例外,在分发中,有一个方法查找,我们在某个类中查找,看方法f,该方法可能有一个返回类型,自类型,这里的类c可能与当前类无关。

我们在这里分发到另一个类,无论当前类是什么,这个特定的自类型指的是该类中的自类型,进行查找的类,不是我们检查类型的任何类,幸运的是,我们永远不需要将该self类型与当前类中的任何self类型进行比较。

因此,不同类型的self类型之间没有交叉,再说一次,这是类型检查规则中唯一查看self类型的地方,这不是当前类中的一个。

总结我们关于self类型的讨论,Self类型仍然是一个研究想法,它为类型系统增加了更多的表达能力,我认为这很容易看到,主流语言中无自类型,自类型本身,我认为不重要,除非你要实现的工程,相反。

包含自类型的原因是展示类型检查可以很微妙,不仅仅是int加int等于int,实际上有相当复杂的东西和相当复杂的推理,实践中类型检查内部在进行,当然类型系统的复杂性和表达能力之间需要平衡,更复杂。

类型系统更难学,使用起来也更困难,但能编写更多程序。

P55:p55 10-06-_Error_Recovery - 加加zero - BV1Mb42177J7

本视频中,我们将结束关于类型检查的系列,讨论如何从类型错误中恢复。

与所有前端阶段如伸缩和解析一样,从类型检查中恢复错误很重要,但与解析不同,从类型检查器中恢复错误要容易得多,因为我们已经有了抽象语法树,因此无需跳过代码的部分,就像我们在解析之前所做的那样。

在知道程序的结构存在问题之前,尽管,应该给无合法类型的表达式分配什么类型,类型检查器通过结构归纳工作,它不能只是卡住,因此,如果我们发现某个子表达式,没有我们可以有意义地赋予它的类型。

我们仍然必须对它做些什么,以便我们可以类型检查,围绕它的所有表达式,一种可能性是简单地分配类型对象作为任何错误类型表达式的类型。

这里的直觉是,即使我们无法确定表达式的类型应该是什么,可以肯定的是,它是某种子类型对象,因此,将任何表达式分配类型对象肯定是安全的,因此,让我们考虑这种策略在简单代码片段中的应用。

所以这里我们有一段小代码,我们假设这里x未定义,实际上代码中有一个错误,那就是x没有绑定,所以x没有任何类型,那么当我们类型检查这个时会发生什么,我们将递归地向下遍历抽象语法树,最终我们会到达叶子。

并尝试类型检查x,然后我们会发现x没有任何地方有类型,这将导致一个错误消息,说x未定义,然后为了继续类型检查以恢复,我们将不得不分配exotype,因此,我们将假设x的类型为对象。

因为那是我们的恢复策略,然后我们将继续类型检查,当我们向上遍历抽象语法树时,接下来我们将尝试类型检查这个加法操作,我们将看到我们正在将类型为对象的东西添加到整数,当然,加法不适用于类型为对象的东西。

因此我们将得到一个错误,类似于加法应用于对象,然后我们现在必须决定,既然我们不能类型检查这个,加法,那么x加二的类型是什么,因此,整个子表达式,当然,我们的恢复策略是,好吧,那也有类型对象, 。

现在抽象语法树中的下一个部分是这里的初始化赋值,我们将y赋值为这个表达式的结果,但我们无法类型检查这个表达式,所以它具有对象类型,现在,类型检查器看到,我们将类型为对象的东西赋值给类型为int的东西。

我们得到了第三个错误,说我们有一种错误的赋值,所以这里的问题是这种简单的恢复策略奏效,如果我们恢复,我们继续类型检查,但一个错误可能引发更多,这是一个可行的解决方案,它,它实现了恢复目标。

但通常会导致连锁错误,一旦有一个类型错误,该类型错误将导致更多,因为对象类型的东西能做不多,代码可能假设更特定类型,这些错误将向上传播至抽象语法树。

不仅导致多个错误,另一种可能是引入新类型,专为错误类型表达式设计的节点类型,无类型并不特殊,不是程序员可用的类型,仅编译器可用,用于错误恢复和类型检查,无类型的特殊性质是,将是其他类型的子类型,所以。

如果你记得对象是相反的,对象是所有类型的超类型,这有坏处,因为对象上定义的方法很少,所以,如果你将类型对象插入到期望其他类型的地方,很可能类型检查不会通过,我们可以通过引入无类型来解决这个问题。

无类型将有特殊属性,即每个操作,每个操作都定义于无类型,此外,我们将说它产生无类型作为结果,所以,语言中任何接受类型参数的操作,无类型,将产生类型结果,无类型,因此节点类型将传播。

现在让我们看看相同的代码片段,让我们分析一下如果我们不使用类型会发生什么,因此我们再次遍历抽象语法树,我们到达这个叶子,X 我们看到X未定义,我们产生一个错误,说x未定义,然后需给x赋类型,因此我们说。

x的类型为,无类型,现在考虑加法操作,现在加法接受类型为,无类型和整数,这样不错,不会报错,被认为是类型正确,结果也为无类型,类型,现在进行赋值,呃,无类型与任何类型不兼容,无类型不是任何类型的子类型。

此赋值也类型正确,该阶段也不会报错,所以你可以看到,节点类型向上传播到抽象语法树,就像对象类型之前一样,但由于无类型是一种特殊类型,仅用于错误恢复,我们可以将其与其他常规类型区分开来。

我们知道不应该在产生第一个错误消息后打印出错误消息。

真正的编译器,生产编译器将使用类似无类型的东西进行错误恢复,但无类型存在实现问题,特别是,无类型是所有其他类的子类型这一事实,意味着类层次结构不再是树,如果你考虑一下,你有一个对象在顶部。

然后有一个树形结构向外分支,但无类型是所有类型的子类型,所以无类型成为底部元素,现在是一个有向无环图,而不是树,这使得实现稍微困难一些,而不是只能使用树算法,现在你必须有,要么为无类型设置特殊情况。

要么做更一般的事情,这只是额外的麻烦,我个人认为不值得为课程项目做,我建议你使用对象解决方案。

P56:p56 11-01-_Runtime_Organiza - 加加zero - BV1Mb42177J7

本视频中,我们将开始讨论运行时系统。

现在这一点,实际上我们已涵盖编译器前端全部,包括三个阶段,词法分析,解析和语义分析,这三个阶段或这三个步骤一起,其工作是真正执行语言语义或语言定义,因此我们知道,这三阶段完成后,若无错误产生。

程序实际上为编译语言的有效程序,此时编译器将能产生代码,生成程序的可执行翻译,应说明的是,当然,执行语言定义仅前端目的之一,前端还构建生成代码所需的数据结构,如我们所见,一旦通过前端,真正变化是。

我们不再试图判断是否为有效程序,现在真正开始生成代码,这是后端的工作,代码生成当然是其中一部分,后端另一大部分是程序优化,进行改进程序的转换,但在谈论任何一件事之前,我们需要谈论,运行时组织。

为什么需要了解,因为我们需要知道,在谈论如何生成和如何有意义之前,我们试图生成什么,首先讨论翻译程序,及其组织结构,然后讨论算法,代码生成,实际产生这些的算法,这是一个理解良好的领域。

至少有一些广泛使用的标准技术,这些是我们将涵盖并鼓励你在项目中使用的。

本系列视频主要内容,是运行时资源管理,特别是,我将强调静态和动态结构的对应和区别,因此,静态结构是编译时存在的,动态结构是运行时存在的,这可能是你最需要理解的区别,如果你想真正理解编译器的工作原理。

编译时发生了什么,运行时发生了什么,在脑海中清楚地分离编译器所做的工作,以及推迟到目标程序或生成的程序实际运行时的工作,这是真正理解编译器工作原理的关键,我们还将讨论存储组织。

所以内存如何用于存储执行程序的数据结构。

所以让我们从开始开始,嗯,所以最初,嗯,操作系统是唯一在机器上运行的程序,当程序被调用时,当用户说他想要运行一个程序时,操作系统将分配空间给程序时发生了什么,程序的代码将被加载到该空间中。

然后操作系统将执行跳转到入口点,或程序的主要函数,然后你的程序将开始运行。

所以让我们看看内存的组织大致是什么样子,当操作系统开始执行编译的程序时,我们将像这样绘制内存的图片,将有一个大块,有一个起始地址在低地址和高地址,这是分配给程序的所有内存。

其中一部分空间将包含程序的代码,程序的实际编译代码将被加载,通常在分配给程序的内存空间的一端,然后有一大块其他空间,嗯,将用于其他事情,我们将在下一分钟内讨论这一点。

在继续之前,我想说几句关于这些运行时组织图片的话,因为我将在接下来的几个视频中画很多这样的图片,所以传统上将内存绘制为矩形,低地址在顶部,高地址在底部,这并没有什么魔力,这只是一种惯例。

我们完全可以颠倒地址的顺序,这没什么大不了的,然后我们将绘制线条来划分内存的不同区域,显示不同类型的数据,以及它们在分配给程序的内存中是如何存储的,显然这些图片都是简化的,如果这个是一个虚拟内存系统。

例如,没有保证这些数据实际上是以连续的方式排列的,但有助于理解,你知道不同类型的数据是什么以及编译器需要做什么,嗯,才能有像这样简单的图片。

所以回到我们的运行时组织图片,我们有一块内存,其中第一部分是程序实际生成的代码,然后有那个其他空间,里面有什么,程序数据在此空间,所有数据在其余空间,代码生成难点在于编译器,负责生成代码和编排数据。

编译器需决定数据布局,并生成正确操作数据的代码,代码中引用数据,当然,代码与数据需共同设计,代码与数据布局,请共同设计,确保生成程序正确运行,现在,实际上不止一种数据,编译器会感兴趣。

下视频将讨论不同种数据,以及数据区不同种类的区别。

P57:p57 11-02-_Activations - 加加zero - BV1Mb42177J7

本视频中,我们将从过程激活的概念开始讨论运行时结构。

在讨论激活之前,明确我们有两个总体目标在代码生成中,一个是正确生成代码,实际上忠实实现程序员的程序,第二个是高效,该代码应充分利用资源,特别是我们经常关心它运行快速,很容易孤立地解决这些问题。

如果我们只关心正确性,生成代码并不难,它非常简单,但也很慢且正确实现程序,如果我们只关心速度,并且我们不关心得到正确答案,问题甚至更容易,我可以生成极快的程序,对任何你关心的问题产生错误答案,因此。

代码生成的所有复杂性都来自于试图同时解决这两个问题,随着时间的推移,已经发展出一个相当复杂的框架,说明如何生成代码和运行时结构,以实现这两个目标,好的,谈论它的第一步是谈论激活。

我们将对生成代码的编程语言类型做出两个假设,第一个假设是执行是顺序的,给定我们执行了一个语句,下一个将被执行的语句,很容易预测,实际上,它只是我们刚刚执行的语句的函数。

因此控制将从程序中的一个点移动到另一个点,遵循某种明确的顺序,第二个假设是当过程被调用时,控制将始终返回调用点后的点,也就是说,如果我执行一个过程,F一旦f完成,执行控制将始终返回调用f的点的下一句。

当然,肯定有违反这些假设的编程语言和编程语言特性,违反假设一的最重要的编程语言类别是那些具有并发性的,因此,在并发程序中,仅仅因为我执行了一个语句,没有,没有简单的方法可以预测下一个将被执行的语句。

因为它可能在完全不同的线程中,呃,对于假设二,呃,高级控制结构,如异常和call cc,如果你碰巧知道什么是call cc,如果违反假设一,一类重要的编程语言是那些具有并发性的,你不做不重要。

影响控制流的结构相当剧烈,也可能违反假设,特别是,如果你熟悉Java和C中的catch和throw异常风格,C++,当我们抛出异常时,异常可能在被捕捉前逃逸多个过程,因此,当你调用一个过程时。

无法保证该过程抛出异常后控制会立即返回过程后的点,本课余下部分将用这些假设,未来视频中会简略讨论如何适应这些高级特性,我们将涵盖的内容是所有实现的基础,即使有并发和异常的语言。

也基于我们将讨论的想法,首先定义,当我们调用过程p时,将称其为过程p的激活,过程p激活的寿命,将是执行过程p所涉及的所有步骤,包括p调用的所有步骤,所以将是所有x,从p被调用至返回的所有语句。

包括所有uh。

p自身调用的函数或过程,我们可以定义变量的类似生命周期,所以变量x的生命周期,将是x被定义的执行部分,这意味着从x首次创建,至被销毁或分配的所有执行步骤,注意,生命周期是动态的,这适用于正在执行的程序。

我们讨论的是变量首次存在的时刻,直到,它消失并超出范围的时刻,另一方面,是一个静态概念,范围指的是程序文本中变量可见的部分,好的,这与变量的生命周期是完全不同的概念,再次,保持这两次很重要。

运行时和编译时发生什么,或与程序静态属性相关,在脑海中区分。

与我们几页前给的假设,我们可以做一个简单观察,即当过程p调用过程q时,然后q将在p返回之前返回,这意味着过程的生命周期将正确嵌套,此外,这意味着我们可以。

用简单例子说明激活,这是一个很酷的程序,和往常一样,它将通过执行,主类中的main方法开始运行,对于这个程序,第一个激活,和激活树的根是main方法,main将调用方法g,g的生存期,g存在的指令集。

将在main的执行期间适当包含,因此,我们可以通过使g成为main的子节点,来表明这一事实,表明main调用g,并且g的生存期,完全包含在main的生存期内,g返回后,main将调用f,因此。

f也将是main的子节点,然后f本身将再次调用g,所以它将有一个节点,我有一个g的另一个激活,因此,g也将是f的子节点,这棵树实际上是完整的树,对于这个特定例子说明了若干件事,首先,如我们已经说过的。

它显示了生存期的包含,例如,g的生存期,包含在main内,但它也显示了一些其他有趣的生存期关系,例如,这个g的激活,和那个f的激活的生存期,是完全不交的,因为它们是在树中的兄弟,它们的生存期完全不重叠。

另一个要注意的是,激活树中可以有相同方法的多次出现,所以每次方法被调用,那都是一个单独的激活,所以在这个特定的激活树中,有两个g的激活,这是一个稍微更复杂的例子,嗯,涉及一个递归函数,让我们从这里开始。

在第一次调用处,所以对main的调用,所有main做的就是调用f,带有参数三,所以有一个f的激活,从main,我就在这里做个笔记,关于论点的一边,因为我们需要跟踪那个,所以f被调用三次,显然那不是零。

然后f将被再次调用,参数为二,这将导致f再次被调用,参数为一,最后f将被调用,参数为零,嗯,这将导致对g的调用,这是这个特定程序的激活树,嗯,再次注意,程序的同一运行中可以有多个过程激活。

这仅仅表明同一个过程可以被多次调用,还请注意,递归过程将导致激活的嵌套,相同的函数内部自身,所以你知道f调用自身,因此第二个对f的调用寿命,正确包含在第一个对f的调用寿命内,总结一下我们关于激活的讨论。

嗯,显然,我认为激活树取决于程序的运行行为,所以它取决于运行时值,确切地说,哪些过程被调用,以及激活树最终会变成什么,现在,这没有在我们的例子中说明,但应该明显的是,对于不同的输入,激活树可以不同。

所以我给你看的程序没有输入,所以每次你运行那些程序,你会得到相同的激活树,但一般来说,如果一个程序接受输入,它将以不同的方式执行,并以不同的顺序调用不同的过程,最后,嗯,这里也许是第一个重要的实现观点。

由于激活是正确嵌套的,我们可以使用栈来实现或跟踪当前活动的激活,所以让我们看看如何使用栈来跟踪激活。

嗯,我们将使用我们之前看过的其中一个例子,我要做的是,我将在这里显示激活树,在左边,我将显示当前执行激活的栈在右边,栈不会跟踪整个激活树,它只会跟踪当前运行的激活,所以在程序的每一步。

栈应包含所有当前活动或运行的激活,所以我们已经看到了如何构建树,我们从执行main开始,所以那将是树的根,由于栈应该包含所有当前运行的激活,栈将必须包含main,所以它将以uh开始。

过程main现在调用g,所以g成为main的子节点,在这里,我们将g推入栈中,然后g返回,这意味着g不再运行,所以g将从栈中弹出,然后main过程调用f,所以f将被推入栈中,你可以看到,在g完成之后。

我们可以弹出它,然后我们可以推入f,我们保持不变,即我们有一个当前运行的激活的栈,然后f将调用g,我忘了完成我的树这里,所以main调用f,然后f调用g,好的,所以现在栈在这个点是,Uh。

main f和g,一旦g完成运行,它将从栈中弹出,因为它不再执行,然后f将完成,f也将从栈中弹出,最后main将完成,main也将从栈中弹出,这就是想法,所以这就是我们可以使用栈的方式,本质上。

当一个过程被调用时,将为该过程在栈上推入一个激活,当一个过程返回时,我们将从栈中弹出该激活,由于激活的生命周期正确嵌套,这将奏效,因此,在我们讨论激活的结论中,让我们回到运行时组织,你可能还记得。

我们为程序分配了一块内存,该块的第一部分由程序本身的代码占据,现在,在分配给程序的其余内存中,我们不得不存储程序需要执行的数据,其中一个重要的结构是激活栈,所以通常这将在代码区域之后开始。

栈将向程序内存空间的另一端增长,当过程被调用时,栈将增长,当过程返回时,栈将收缩,正如我们将看到的,数据区还有其他内容。