威斯康星-CS536-编程语言与编译器笔记-全-
威斯康星 CS536 编程语言与编译器笔记(全)
001:课程介绍与概述 🎓



在本节课中,我们将学习CS536课程的基本信息,包括课程结构、教学团队、学习资源以及一些重要的学习建议。本节内容旨在帮助你快速了解课程全貌,为后续的学习做好准备。




课程概述



CS536是编程语言与编译器导论课程。我是查尔斯·费舍尔,本课程的讲师。我看到了一些熟悉的面孔,因为上次我没有让那么多学生不及格。同时也有很多新面孔,我相信我会有机会认识大家。

课程网站与资源



接下来,我将引导大家浏览课程网页。当我们需要查看具体笔记时,你应该已经拥有这个URL。大约一周前,我通过Piazza向你们发送了欢迎信息,其中包含了课程URL。我在这里再次强调,是为了确保你们知道在哪里可以找到所需资料。

我们还有课程助教和评分员,我想向大家介绍他们,以便你们能将名字和面孔对应起来。



教学团队介绍



我们的教学团队由讲师、助教和评分员组成。


- 讲师:查尔斯·费舍尔。我的办公室在计算机科学系,但这一点不太重要,因为我们将课程带给大家,而不是让大家来找我。我将在每周二在这里授课,并在周一和周四在走廊对面的Ends游戏室进行答疑。
- 助教:奥克沙亚·卡亚·纳玛兰。她将担任助教,这意味着她将在我没有答疑的日子(周三和周五)的同一时间、同一地点进行答疑,时间是下午5:30到7点。
- 评分员:简先生。他的头衔是评分员,主要负责作业批改。他不会单独安排答疑时间,但偶尔会在我答疑时一同出现。特别是当他批改完某项作业后,如果你们对评分有疑问,可以在那个时间段直接与他交流。我会在Piazza上发布他何时会出现的通知。
答疑时间与学习建议
许多上过这门课的同学知道一些技巧。答疑时间当然是为了回答问题,但许多学生也将其用作一种自习室。




这不是高中那种因为被抓到抽烟而被送去的那种自习室。事实上,就在我们离开校园时,我们大楼的火警响了,这通常是因为有人想抽烟,偷偷溜进洗手间,却没注意到头顶上巨大的烟雾探测器,一点火,几秒钟内警报就响了。





我推荐的自习室是一个你可以远离办公室、专注于项目、材料、作业或其他事情的地方。在那里,电话不会响(除非有人知道你的手机号)。另一个好处是,有时别人问的问题会让你想:“嘿,我从来没想过那个。他在问怎么做这个,我也不知道。也许我最好听听看是怎么回事。”所以,你们当然可以来,甚至可以不和我说一句话。你们可以进来,打开笔记本电脑开始打字,看看手表,到时间去吃晚饭然后离开,这都没问题。如果你们有问题,我或助教将随时准备回答。

在周末,你们基本上要靠自己了,但也不完全是。我马上会讲到关于Piazza的幻灯片。




在线交流平台:Piazza


任何以前上过我课的人都知道Piazza。如果你上过其他计算机科学课程,几乎所有的课程都使用Piazza。这是一种问答公告板系统,你可以在上面阅读人们提出的问题并查看答案。

我想强调一点:只要你们对自己的答案有相当的把握,欢迎你们自己回答问题。


例如,我大部分开发工作都在Mac上进行,但你们中有些人使用Windows系统,可能是Windows 7或Windows 10。上次甚至还有几个人用Windows 8,并且说他们喜欢它。我不确定我完全相信他们,也许他们持有微软的股票。


因此,可能会有一些情况在这些平台上略有不同。如果你熟悉并知道答案,请积极回答。有时就是这些问题。比如“我正在尝试下载Eclipse,但出了点问题”。我认为在Windows系统上,有时需要设置一些环境变量,才能让它找到Java编译器和Java运行时。如果你没有这个问题,那很好。如果你有,几乎肯定有人知道如何回答。所以如果是这种情况,请这样做。

作业与考试安排





这次我们还有一位评分员,就是简先生。他不会安排答疑时间,因为他的正式头衔是评分员。他的工作是批改作业。过去,由于班级规模,有时作业批改需要一段时间。我认为现在有了两个人,情况会好一些。他们将分摊作业批改任务。我们会有五个Java编程项目和三个侧重于理论材料的作业。他们将批改这些作业,希望在完成和反馈方面能更快一些。



至于考试,包括期中考试和期末考试,都由我自己批改。所以,我会在我完成的时候完成批改。但至少对于期末考试,通常是在学期结束前几天,这是一个很好的提示:如果你在那之前还没交卷,学期就结束了,你将失去最后的机会。所以我会在那个时间点完成批改。
教材信息


这是教材。我认识所有三位作者,其中两位我可以肯定地评价他们是绅士和学者,两人都非常博学。


在Epic图书馆应该有一本副本,但我从未找到过,不过据说它存在。那栋楼是安德罗梅德大楼,很容易找到,所有的招待会都在那里举行。所以那里至少应该有一本,也许更多。这本书已经出版几年了,所以如果你想找二手书应该不难。当然也有新书。
但教科书的价格已经失控了。我不知道,有没有人知道,这是否属于那种可以电子租赁访问而不是购买的类型?越来越多的教科书提供这种属性,你可以以更适中的费用,通过某种阅读器租赁访问,这样你就能合法地使用这本书,而无需支付那么多钱。我认为这可能是一种可行的方式。但我还没有去查证。
总结




本节课中,我们一起学习了CS536课程的基本框架。我们认识了教学团队的成员,了解了答疑时间的安排及其作为自习室的用途。我们还介绍了Piazza在线平台,鼓励大家在上面积极提问和互助。最后,我们明确了作业、考试的结构以及教材的获取方式。希望这些信息能帮助你顺利开始本课程的学习。
002:课程安排与项目开发方法 🎯
在本节课中,我们将了解课程视频资料的新发布方式,并重点学习一个至关重要的软件开发实践方法。
课程资料发布说明 📢
上一节我们介绍了课程的基本情况,本节中我们来看看课程资料发布方式的更新。
学校推出了新的座右铭“不断向前”。我们改进了录音方式,从单一的音频录制转变为结合音频与屏幕内容的视频录制。目前屏幕上显示的内容将取代之前的纯音频录音。这个系统会自动录制屏幕内容和开启麦克风时的音频。后台工作人员需要找到大致的时间点,进行剪辑,最终生成代表数小时课程的内容。
我们一直在研究分发这些资料的最佳方式。与所有新改进一样,这带来了一些变化。以前,MP3格式的音频文件大约为150到200 MB。MP3的规则大约是每分钟1 MB。而现在包含视频和音频、长达数小时的文件大小为2.2 GB。
今天下午我在家尝试上传时遇到了困难。虽然下载速度尚可,但大多数无线网络的上传速度远低于下载速度,导致我无法将文件上传到威斯康星大学的服务器。我相信如果我在学校,或者使用这里可能容量更大的无线系统,就可以完成上传。
作为备用方案,资料通过Google Drive分发给我。以下是关于使用Google Drive的说明:
- 课程网页上可能会提供链接,或者同时提供Google Drive的访问方式。
- 我们将建立一个对CS536课程所有学生开放的Google Drive文件夹。
- 你需要打开Google Drive,找到相应的文件夹和标有讲座日期的特定文件。
你可以选择观看视频,或者忽略视频、只收听音频,就像收听纯音频录音一样。
这种文件大小的变化让我思考数据消费的演进。我过去想知道人们如何处理几十兆字节的数据,后来发现是音频和照片。现在,人们处理千兆字节的数据,例如这个视频文件。问题是,接下来人们将如何处理数十、数百或数千千兆字节的数据呢?我认为下一个巨大的数据消费点可能是完全沉浸式的3D视频。
完全沉浸式的概念与许多人在剧院体验过的3D技术不同。在完全沉浸式中,你可以移动、转身等。例如,在迪士尼世界的EPCOT中心有一个叫“Circle Vision”的项目,它向一个圆形屏幕投影。如果他们正在展示在中国乘船溯流而上的场景,我可以看向前方看到目的地,也可以转身看到来路。而现在,他们正考虑加入让用户移动的功能。
试想一下,如果《拯救大兵瑞恩》采用完全沉浸式3D技术会怎样。这部电影的前15到20分钟非常震撼,它描绘了士兵在D日海滩登陆并遭受猛烈攻击的场景。事实上,它过于真实,以至于我一位朋友那代人母亲不得不离开影院,因为她认识一些中弹身亡的士兵,再次看到这样的场景让她难以承受。在完全沉浸式3D中,你可以移动,可以走向这里或那里,可以回到海边看看那里的情况,甚至可以爬上悬崖看看德军在做什么。
这不仅仅是娱乐。试想一下,如果用于培训警察、消防员、士兵或任何需要应对突发实时状况的人员,那会怎样。这有点像《星际迷航》中的全息甲板,但你不会遇到可以撞击你的实体形象。你会像一个在环境中漂浮的幽灵。这将消耗大量的字节、比特和计算能力,而人们会很乐意出售这些资源。
无论如何,我可能会在明天或周四发送关于此事的消息,包括第一讲和第二讲(即现在正在进行的课程)的资料获取方式。可能是通过Google Drive,或者如果我能建立正确的连接,我会直接上传到学校的服务器,这样课程网页就能提供良好的下载速度。
我提醒你,你可能希望实时流式播放这些内容。即使在我家相当不错的Wi-Fi连接下,下载这个2.2 GB的文件也花了我大约30分钟。这个数据量确实很大,而网络通常并未为此类大文件传输进行优化。再次强调,当我们实现完全沉浸式3D时,文件将不是几个GB,而可能是100 GB,甚至一个TB,那时你将需要更好的带宽来处理所有这些数据。
以上说明是为了告诉大家目前的进展。许多人都在问第一讲的资料在哪里,我们正处于过渡期,正在与相关人员协调并了解全部情况。
项目开发方法 💡
在正式讲座开始之前,我想先就项目作业做几点说明。我手头有几份项目说明的复印件。上次课已经分发过,如果你现在举手,意味着你上次没来。如果你保证下次不再缺席,我可以原谅你。当然,这份说明也可以从课程网页下载。
我们还有一个新的文档摄像机,据说分辨率比之前更高。我会尝试使用它。
首先,现在有些人已经开始做项目一了,我想重申一个许多你们可能通过惨痛经历才学到的真理,也许有些人还没经历过。这是普遍真理之一,而普遍真理相当罕见,它关乎软件开发。
存在正确的方法,当然也存在错误的方法,或许应该说存在更好的方法。不幸的是,错误的方法在初学时似乎很直观,那就是先写出整个程序,然后进入编译器和调试器开始调试,接着测试它。这对于小程序来说没问题,但随着程序越来越大(在课程结束前,你们可能会有五千到一万行代码,开始时这看起来很多),这种方法就行不通了。有谁知道Epic公司所有产品总共有多少行代码吗?答案是数百万行。事实上,我有个学生在一个非常古老的程序中看到了一小段代码,上面的注释是“JF”。JF可能是谁呢?是Judy。这就像在旧记录中,在某个法院找到亚伯拉罕·林肯签署的文件一样。
无论如何,代码量非常庞大。因此,另一种方法,也是我要谈的,并给你们一些如何进行的见解的方法是:逐步进行修改或添加。显然不是一次只加一行,但也不要试图一次性完成所有内容。这意味着,在处理一个项目时,在达到最终产品之前,拥有一个甚至多个中间步骤通常是好的。
我们正在做的是,我给了你们一个进行抽象语法树遍历的代码版本。
本节课中我们一起了解了课程视频资料发布的新流程,并重点学习了“增量式开发”这一核心软件开发实践。记住,不要试图一次性编写和调试大型程序,而应通过构建和测试小块功能来稳步推进,这对于管理复杂项目至关重要。
003:词法分析器实现与有限自动机 🧠
在本节课中,我们将学习词法分析器的核心实现机制,特别是如何将有限自动机(Finite Automaton)的逻辑转化为实际的代码。我们将探讨两种实现方式:基于状态转移表的“显式控制”和由工具(如JLex)自动生成的“代码式”逻辑,并分析各自的优缺点。







词法分析器的核心循环




上一节我们介绍了有限自动机(FA)作为词法分析器的理论基础。本节中我们来看看如何将这个理论模型转化为一个可以运行的扫描器程序。
其核心是一个循环,它不断读取输入字符,并根据当前状态和字符查找状态转移表来决定下一个状态。



以下是该循环的基本逻辑,用伪代码描述:


state = 初始状态
读取一个字符 ch
while (true) {
if (ch 是文件结束符 EOF) {
break; // 无法继续读取,跳出循环
}
next_state = 状态转移表 T[state][ch];
if (next_state 是空白或错误状态) {
break; // 遇到非法转移,跳出循环
}
state = next_state;
读取下一个字符 ch;
}
// 循环结束后,判断最终状态
if (state 在可接受状态列表中) {
识别出对应的词法单元(Token)并进行处理;
} else {
报告词法错误;
}



这个循环会一直运行,直到遇到文件结束符(EOF)或查表时发现一个非法的状态转移(例如,表中对应项为空白)。跳出循环后,程序会检查当前状态是否为“可接受状态”。如果是,则成功识别出一个词素;否则,意味着输入的字符序列不符合任何词法规则,需要报告错误。




两种实现策略:表格驱动 vs. 自动生成代码



理解了核心循环后,我们面临一个实现选择:是手动编写一个基于状态转移表的扫描器,还是使用工具来自动生成扫描器代码?


显式控制(表格驱动)



第一种方法是“显式控制”。我们手动(或通过程序)构建出完整的状态转移表,并在扫描器的主循环中查询这张表。这种方法的优势在于逻辑清晰,控制流完全暴露在代码中,便于人类阅读和理解。
公式: 扫描过程可以形式化地表示为:下一个状态 = T(当前状态, 当前字符),其中 T 是状态转移函数,通常用二维数组(表)实现。


自动生成代码(如JLex)



第二种方法是使用像 JLex 这样的扫描器生成工具。我们只需要用类似正则表达式的语法定义词法规则,JLex 就会自动为我们生成执行扫描的 Java 代码。

这种方法生成的代码可能不再是简单的查表循环,而是一系列嵌套的 if-else 或 switch-case 语句,直接编码了状态转移的逻辑。例如,对于识别“//”注释的逻辑,生成的代码可能类似:


if (currentChar == '/') {
readNextChar();
if (currentChar == '/') {
// 进入单行注释处理
while (currentChar != EOF && currentChar != '\n') {
readNextChar();
}
// 成功识别出一个注释Token
}
}




为何倾向于使用生成工具?

你可能会想:“能直接阅读和修改代码不是很好吗?” 这确实是一个优点。但让我们从软件工程,特别是质量保证(QA)的角度来思考这个问题。
- 避免引入新错误: 当需要修复一个bug时,直接修改复杂的扫描器逻辑存在风险。修复一处代码可能会无意中破坏另一处依赖特定假设的逻辑,就像试图压平一块翘曲的木板,钉下一头时另一头又翘了起来。
- 维护与测试成本: 手动编写的扫描器代码在词法规则变更时需要同步更新,且必须重新进行全面的测试,以确保修改没有破坏原有功能。这个过程容易出错且耗时。
- 关注点分离: 使用JLex,开发者只需关注词法规则的定义(即“要识别什么”),而无需操心具体的识别逻辑(即“如何识别”)。只要规则定义正确,生成的扫描器就是正确的。如果生成的扫描器有问题,根源在于生成工具,而非我们的规则。



因此,自动生成代码的方式虽然牺牲了一点“可读性”,但换来了更高的可靠性、可维护性和开发效率。在本课程中,我们将使用JLex,你无需查看或理解它生成的具体状态表,只需学会如何定义词法规则即可。



实例解析:实数常量的识别



让我们通过一个更复杂的例子来巩固理解:如何识别一个实数(浮点数)常量。



假设我们的规则是:实数必须包含一个小数点,并且小数点前后至少有一边要有数字(即不允许单独的一个点“.”)。


用正则表达式可以定义为:(digit+ '.' digit*) | (digit* '.' digit+)
digit表示数字+表示一个或多个*表示零个或多个|表示“或”


这个表达式对应的有限自动机状态图比简单的查表循环更直观地展示了识别路径:
- 从起始状态开始,读入数字可以进入一个可接受状态(识别为整数)。
- 也可以读入数字后读入小数点,再读入数字,进入另一个可接受状态(识别为小数)。
- 或者直接读入小数点,但必须后跟至少一个数字才能进入可接受状态。
- 单独的一个小数点无法到达任何可接受状态,因此会被拒绝。

与看正则表达式相比,状态图能更清晰地展示“不允许单独小数点”这一约束是如何实现的。在接下来的实践中,我们将学习如何用JLex定义这样的规则。




总结与预告

本节课中我们一起学习了:
- 词法分析器核心循环的实现逻辑,它基于状态转移表驱动。
- 实现词法分析器的两种策略:表格驱动的显式控制和使用JLex等工具自动生成代码。
- 分析了自动生成代码方式的优势:降低错误风险、便于维护、实现关注点分离。
- 通过实数常量识别的例子,观察了正则表达式与有限自动机状态图之间的联系。



在接下来的课程中,我们将正式开始使用 JLex 工具。你将学习如何编写词法规则定义文件,并利用它自动生成你的第一个扫描器,为后续的编译器项目打下坚实的基础。
004:JFlex扫描器与作业指南 🛠️




在本节课中,我们将学习如何使用JFlex工具构建扫描器(词法分析器),并了解与第一次作业和第二个项目相关的核心概念。课程内容将涵盖JFlex的基本工作原理、处理词法规则冲突的策略,以及如何实现简单的错误处理。


课程内容概述


上一节我们介绍了词法分析的基本概念。本节中,我们将深入探讨JFlex扫描器的具体实现细节。


首先需要说明上周讲座视频的技术问题。上周发布的讲座分为三段,但只有第三段(约最后一小时)包含了视频内容,所有三段都包含音频。这是由于技术故障导致的,可能是连接断开或存储空间不足。缺失的视频部分可以通过音频来弥补。




此外,编辑视频的人员可能未意识到课程中间有休息时间,因此在视频的小时间隔处会出现无音频和画面的固定屏幕。遇到这种情况时,请快进跳过这些部分。


项目一回顾与项目二介绍




本节课的主要目标是涵盖完成第一次作业和第二个项目所需的全部材料。第一次作业涉及处理 /* ... */ 格式的注释,第二个项目则是构建扫描器。
部分同学可能已开始项目二,部分同学可能仍在完成今天截止的项目一。首先,关于项目一是否有任何未解决的问题?或者任何普通问题?如果大家都没有问题,或者不愿公开提问,也可以在课间私下询问。





接下来,我们继续讨论项目二。项目二的核心是学习如何使用JFlex。



JFlex与正则表达式

我之前提到,正则表达式是定义各种词法单元(字符串集合)的便捷方式。JFlex是一个特定工具,它将正则表达式符号转化为实际的编程语言。它并非纯粹的Java,而是部分Java,部分JFlex自有语法。其核心是:你定义一个词法单元的模式,并指定匹配该模式时应执行的操作,而无需你亲自负责区分不同词法单元——这完全取决于模式是否能匹配输入的字符序列。
稍后,我将讨论当模式出现重叠时会发生什么,而这在实际中是必然会遇到的情况。即便如此,规则也极其简单:总是匹配最长的可能字符序列。
扫描器的工作原理与责任边界
关于这一点,我想澄清一个之前被问到的问题:构建扫描器时,你的责任不是让扫描器感知在语法分析器层面什么是有效或无效的。
举个例子,考虑输入 101bool。如果中间有空格,101 会被扫描为整数字面量,bool 会被扫描为保留字。但如果没有空格,规则是:扫描器会持续读取,直到无法继续匹配为止。101 是有效的整数字面量,但 101b 可能是一个位值(bit-valued)或布尔值整数字面量的表示(例如,b 表示只允许数字0和1)。扫描器会继续读取 o,发现无法继续,因此 101b 是一个有效的位字符串字面量。由于它比 101 更长,根据“最长匹配”原则,它会胜出。
实际上,你必须遵循这个原则。否则,像 bool 这样的简单词可能被错误地扫描为四个独立的标识符(b, o, o, l),你将几乎无法得到超过单字符的词法单元,这显然不是我们想要的。
规则是:尽可能长地扫描,但不要试图判断后续的语法合法性。 扫描器不会尝试判断“这个位字符串字面量后面跟着标识符 OL 是否合法”。这不是扫描器的责任。语法分析器会检查这一点。事实上,在我们的语言中,这种组合可能不合法,语法分析器会报错。但扫描器不知道语法分析器的规则。也许在语言的某个变体中,这是允许的。因此,扫描器只完成自己的工作,不越俎代庖。
在大型团队项目中,这一点尤为重要。每个成员负责自己的模块。如果出现错误,必须有人负责处理,但在我们的规则中,这不是扫描器的责任,而是语法分析器的问题,由它来判断“这个整数字面量和这个标识符不能相邻出现”。通常,中间需要某种标点符号或运算符,但这由语法分析器来规定合法性。
处理词法冲突与保留字
除了“最长匹配”规则,还有第二条规则来处理冲突,特别是涉及保留字(如 bool)和通用标识符模式的情况。bool 可能被误解为一个标识符。我们通过以下规则解决:如果一个字符串可以以两种不同方式扫描(例如,作为保留字 bool 或作为标识符 bool),则定义顺序靠前的规则优先。
因此,在定义保留字这类词法单元时,通常将它们放在所有有效词法单元列表的很靠前的位置。
我们还可以利用这个技巧来实现一些相当简单的错误处理。有些字符,除非它们出现在引号内的字符、字符串或注释中,否则本身是无效的。例如,单独的 # 号(被空白包围)可能是非法的。
以下是处理方式:在所有有效词法单元规则之后(我会稍后展示),我们添加一个匹配任何单个字符的规则。其原理是:如果一个有效单字符(如逗号 ,)有其更早的定义,它会被前面的规则捕获。这就像在“塔可星期二”去食堂晚了,好的配料(鳄梨酱、酸奶油)都没了,只剩下一些不太想要的配料。
同样在这里,如果前面的所有规则都有机会匹配 # 号,但都没有匹配成功,那么最后这条“任何单字符”规则就会匹配它,并通常生成一个错误信息,如“第X行,第Y列,非法字符”。你的处理方式是:跳过它。你无法理解它,所以跳过它。这可能不是最完美的处理方式,但你肯定不希望仅仅因为输入了一个非法字符就停止整个编译过程。对于像我这样的糟糕打字员,有时会误按两个键,产生额外字符。优秀的打字员可能没有这个问题,但这类情况确实存在。
本节的关键要点是:不要试图过度智能化系统。匹配合法的内容,并以我们规定的方式识别非法内容。 我们会讨论如何处理更复杂的错误,但错误越复杂,识别它就越困难,更不用说判断具体错在哪里了。
如果你做过其他软件项目,或者使用过某些医疗产品,就会遇到类似问题。例如,医生或护士输入患者治疗信息时,可能输入了完全非法的内容,比如在“休息小时数”一栏输入了 #。这可能意味着他们想输入数字 3(因为 # 在键盘上 3 的上方)但按错了Shift键,但也可能不是。在这种情况下,如果规则规定那是非法的,你的责任就是指出它非法。在简单情况下,你或许可以尝试修复,但试图猜测用户的真实意图是困难的,尤其是在医疗保健等领域,这可能对治疗过程产生重大影响。你的责任不一定是弄清楚医生或护士想要什么,而是说:“我期望得到合法输入,但没有找到,请告诉我该怎么做。”
项目二具体要求
现在,让我们回到项目二的具体要求。我们已经将项目一置于身后。
我已经提供了一个可工作的扫描器基础代码。这个扫描器目前只处理少数几种词法单元,但它包含了完整的JFlex规范框架,甚至在底部已经包含了一点错误处理代码,这正呼应了我刚才讨论的内容。




本节课中,我们一起学习了JFlex扫描器的核心构建原则。我们明确了扫描器的责任边界(“最长匹配”,不进行语法判断),掌握了处理词法规则冲突的策略(定义顺序优先),并了解了如何通过兜底规则实现基本的非法字符错误处理。这些知识是完成项目二(构建完整扫描器)和第一次作业(处理特定注释)的基础。请记住,保持模块职责清晰是构建复杂系统的重要原则。
005:项目2常见问题与字符处理


在本节课中,我们将学习与项目2相关的几个重要知识点,特别是关于不同操作系统下文本文件格式的差异,以及如何在词法分析器中正确处理这些差异。
操作系统与文件格式差异
上一节我们介绍了课程背景,本节中我们来看看一个在项目开发中可能遇到的、由操作系统差异导致的具体问题。
如果你在Windows操作系统上使用Eclipse进行开发,可能会遇到一个虽小但令人困扰的问题。这与DOS(Windows系统的前身)与Mac、Unix系统之间普通文本文件的文件系统格式差异有关。
Mac系统底层实际上是Unix系统,拥有一个精美的图形用户界面。而DOS和Windows则是完全独立的产品,这也是它们达到Unix系统可靠性水平耗时较长的原因之一。Unix系统可追溯到20世纪70年代,虽然历史悠久,但长期的使用也意味着其主要错误和大部分次要错误都已被发现并修复。
从我们的角度来看,关键差异在于行结束标记。在Unix系统中,行结束标记是换行符(\n)。而在DOS/Windows系统中,行结束标记是回车符与换行符的组合(\r\n)。
这两种选择都是合理的,就像有人喜欢在薯条上加番茄酱,而有人(例如英国的炸鱼薯条)更喜欢加醋一样。问题在于,如果你在Windows系统上使用Windows编辑器,它通常会采用标准的DOS/Windows文件约定,即包含回车符。
在扫描器中处理回车符
这本身没有问题,但当你将其输入到我们生成的扫描器(词法分析器)时,情况就不同了。在Mac OS/Unix环境下,回车符并不常用,因此我在规范中并未特别提及它。
你真正需要做的是,在扫描时意识到可能会遇到回车符。我认为最好的处理方式是匹配它并忽略它,就像处理空白字符一样。同时,你需要仔细检查以确保行号计数正确。
以下是需要特别注意的逻辑:
- 当你看到换行符(
\n)时,应增加行号计数器,并将列号重置为下一行的起始位置(例如第1列)。 - 当你看到回车换行组合(
\r\n)时,你只应执行一次行号递增操作,而不是两次。
如果你不忽略回车符(\r),会发生什么?这个字符将无法被任何词法模式匹配。JLex(我们使用的扫描器生成器)有一个规则:如果生成的扫描器(即YYlex)完全无法匹配一个字符,它基本上会终止扫描过程,这会导致一个运行时错误。
防止非法字符的错误处理
虽然这个规则有些武断,但有一个非常简单的解决方法可以避免扫描意外终止,并输出有意义的错误信息。以下是一种有效的保护机制,你可以直接使用或参考其思路。
这个思路实际上来源于项目1的代码。请记住,项目1中提供给的所有内容都是开源的,你可以自由使用和借鉴。其中包含了一个用于测试项目1的CSX-Lite版本的JLex规范。
如果你查看该规范的底部,会发现几行代码,它们本质上保护程序免受非法字符的影响。词法分析规则是从上到下应用的,第一个匹配字符的模式将处理该字符。因此,如果扫描进行到底部,意味着没有其他模式想要匹配当前字符,这可能就包括了回车符(\r)。
以下是核心的防护代码逻辑:
. {
// 发生词法错误:无法匹配的字符
System.err.println("Lexical error at line " + yyline + ", column " + yycolumn + ": Unrecognized character '" + yytext() + "' ignored.");
yycolumn++; // 因为忽略了一个字符,但列位置需要前进
}
代码解释:
- 模式
.匹配除换行符(\n)外的任意单个字符。它之所以停在行尾,是因为如果你想匹配包括换行符在内的所有字符,需要使用类似(.|\n)的模式。 - 在这段保护性代码中,我们报告一个词法错误,输出行号、列号以及无法识别的字符内容(
yytext())。 - 即使忽略了这个字符,我们仍然将列号加一,因为确实有一个字符存在,扫描需要继续向前进行。
这几行代码是一个非常简单的方法,可以保护你的扫描器免受完全非法的字符,或者那些你打算匹配但尚未编写对应规则的字符的干扰。此外,如果你查看项目1的JLex定义,还会发现一些关于保留字等内容的示例,它们比项目2的初始定义更详细,也值得参考。
本节课中我们一起学习了如何处理跨平台开发时由行结束符差异带来的问题,并掌握了一种通用的、防止未识别字符导致扫描器意外终止的错误处理机制。这些技巧将帮助你更稳健地完成项目2的词法分析器部分。
006:课程公告与工具设置 🎬




在本节课中,我们将处理课程相关的公告,并完成录制工具的初始设置,确保后续课程能顺利进行。
课程公告 📢
首先,有一项关于教室变更的公告需要通知大家。由于下周本教室有特殊活动安排,我们的课程地点需要临时调整。具体的新教室信息,请留意后续的官方通知。

工具设置与调试 🛠️

上一节我们介绍了课程安排,本节中我们来看看录制工具的设置过程。为了找到最佳的视觉效果,我们对视频的显示模式进行了测试和调整。


以下是测试不同显示模式的过程:
- 我们首先尝试了“反色”模式,这种模式模拟了黑板(深色背景,浅色文字)的效果。
- 随后,我们切换回了常规的“白底黑字”模式进行对比。
- 经过比较,常规模式被认为具有更好的可读性,因此我们决定采用此设置。
在确认显示模式后,我们还需要确保音频等所有录制功能正常工作。一切就绪后,我们将正式切换到主讲画面,开始课程内容的讲解。
总结 📝

本节课中我们一起学习了临时的教室变更信息,并完成了课程录制工具的基本设置与调试,包括选择可读性更佳的常规显示模式。这些准备工作将为后续深入的技术内容讲解奠定基础。
007:项目三说明与语法设计
在本节课中,我们将详细讲解项目三的规范说明,并深入探讨其中涉及的语法设计决策,特别是如何处理语法中的歧义和特殊结构。
概述
首先,我们回顾并分发项目三的说明文档。这份文档定义了我们即将实现的微型Java语言的语法规则。在讲解过程中,我会指出文档中几处需要特别注意的地方,并解释其背后的设计原因。
项目文档说明
上一节我们分发了项目三的纸质文档。现在,我们来关注文档中的几个关键点。
以下是关于文档格式和内容的两个重要说明:
- 符号显示问题:在最初打印的版本中,表示“空”或“无”的可选符号(λ,即lambda)可能显示不清。此问题在在线版本中已修复。
- 特殊的语法规则:文档中用于定义类成员(字段和方法)的两条产生式看起来有些特别,但它们的设计是正确且有原因的。
语法设计详解
上一节我们提到了特殊的语法规则,本节中我们来看看其具体形式和设计考量。
在定义类体时,语法规则将字段声明和方法声明分开处理,不允许像完整Java语言那样混合编写。这虽然是一个限制,但有时能增强代码的可读性。
核心的语法规则片段如下:
ClassBody -> FieldDecls MethodDecls
FieldDecls -> FieldDecls FieldDecl | λ
MethodDecls -> MethodDecls MethodDecl | λ
设计原因:这种看似冗余的结构是为了避免使用Java CUP(一种LALR解析器生成器)时产生“移进-归约”冲突。如果采用更直观的写法(例如直接使用FieldDecl*和MethodDecl*),解析器将无法在遇到一个新声明时,确定应该将其归约为一个字段声明序列的结束,还是移进以开始一个新的方法声明序列。当前的设计巧妙地解决了这个二义性问题。
在构建抽象语法树时,你需要对这种结构进行一些额外的处理,但语法本身是有效且无冲突的。
语法歧义性处理
我们刚刚讨论了如何通过设计避免一种解析冲突。现在,我们来看看语言中一个固有的、无法通过文法规则消除的歧义。
这就是“if-then”和“if-then-else”语句的经典歧义问题。考虑以下代码:
if (condition1)
if (condition2)
statementA;
else
statementB;
问题是:else子句应该与第一个if还是第二个if配对?
歧义的本质:对于同一段输入,存在两种不同的语法推导树(即两种解释)。这违反了编译器设计的原则——程序的含义必须是明确唯一的。否则,程序员的理解可能与编译器的解释不同,导致难以察觉的错误。
在编程语言设计中,我们致力于消除歧义,确保所有程序都有清晰、唯一的解释。这类似于乔治·奥威尔在《1984》中描述的“新话”理念,即通过限制语言的表达能力来防止“错误”思想的产生。在编程中,我们通过严谨的语法设计,防止程序员写出含义模糊的代码。现代IDE(如Eclipse)的许多警告(例如“变量可能未初始化”)也是这种思想的体现,旨在阻止可能导致未定义行为的程序被编写出来。
对于if-else歧义,Java CUP等工具提供了优先级和结合性规则来指定解决方案(通常规定else与最近的if配对)。你需要在项目实现中应用这一规则。
项目核心任务总结
本节课中我们一起学习了项目三的背景知识和关键难点。
你的核心任务是:将项目说明文档中大约两页的语法产生式(从第一页中部到第三页顶部),转化为Java CUP可识别的格式。在此过程中,你需要:
- 编写
Java CUP规范文件来定义语法。 - 在语法动作中,构建与项目一结构相似的抽象语法树。
项目一提供了现成的AST节点类和遍历框架,你的工作重点在于正确解析输入并生成对应的AST结构,而无需像项目一那样实现复杂的语义分析。
总而言之,本项目是连接词法分析(项目二)与后续语义分析的关键步骤,你将亲手实现语法解析器,将字符序列转化为结构化的程序表示。
008:项目工具使用与语法树构建解析
在本节课中,我们将学习两个关键内容:一是如何利用项目中提供的图形化工具来更高效地测试你的解析器;二是深入解析一个在构建抽象语法树时可能遇到的棘手语法规则,并提供清晰的解决方案。
项目测试工具介绍
上一节我们讨论了项目的基本结构,本节中我们来看看如何利用已集成的工具来简化测试流程。项目中包含一个名为 ArgsProcessor.java 的文件,它提供了便捷的图形用户界面来运行测试,无需反复修改构建配置文件。
以下是两种可用的GUI模式:
- 简单文件选择器:弹出一个窗口,允许你直接输入要测试的文件名。
- 文件浏览器:一个更实用的界面,允许你浏览目录并点击选择要测试的文件。
要使用文件浏览器,你需要在 build.xml 文件中运行名为 test1 的目标。这个操作会编译必要的内容,然后启动文件选择窗口。在提供的项目文件中,有一个名为 testfiles 的目录,其中包含了许多预先编写好的测试用例,你可以直接选择它们进行测试。例如,选择 test1 文件并点击“打开”,系统将处理该文件,如果解析成功,会输出相应的结果。这种方式让你可以轻松地组织和管理自己的测试文件。
棘手的语法规则与AST构建
现在,让我们转向项目实现中的一个具体挑战。许多同学在构建抽象语法树时,对语法中的第二条产生式感到困惑。这条规则涉及 memberDecls(成员声明),其结构定义包含两个子部分:fieldDecls(字段声明列表)和 methodDecls(方法声明列表)。
然而,在具体的产生式 memberDecls -> fieldDecl memberDecls 中,右侧并没有直接出现 fieldDecls,而是只有一个单独的 fieldDecl 和另一个 memberDecls。这种设计的意图是以从右向左的方式,将字段逐个添加到成员声明列表中。首先,通过另一条产生式 memberDecls -> methodDecls 找到所有方法。然后,每当遇到一个字段声明,就将其添加到现有成员列表的头部(即字段部分的最左端)。
你可能会问,为什么采用这种看似不直观的方式?原因与空产生式有关。如果采用更直观的写法,会因为 fieldDecls 和 methodDecls 都可能推导为空而产生歧义,使得语法无法被确定性地解析。空产生式就像“幽灵”,因为你看不到任何标记来指示它的存在,这会给解析器带来巨大挑战。我们将在后续课程中详细讨论如何处理空产生式。这里采用这种结构是为了确保语法的可解析性。
因此,你的任务不是修改解析器,而是正确地构建AST。对于大多数产生式,构建AST很简单:直接调用对应节点的构造函数并传入参数即可。但对于 memberDecls -> fieldDecl memberDecls 这条规则,你需要进行一些“编辑”操作。
AST构建策略
具体来说,当处理这条规则时,你手头有一个新的、单独的 fieldDecl 节点,以及一个已经部分构建好的 memberDecls 子树(它包含了更靠右位置出现的所有其他字段和方法)。你的目标是创建一个新的 memberDecls 节点。
建议的处理方法如下:你已经有一个 memberDecls 对象,现在需要编辑它,将新的单个字段插入到其字段列表的最前端。你可以将 fieldDecls 视为一个链表,新字段需要成为这个链表的头节点。这类似于你在数据结构课程中向链表头部添加节点的操作。虽然这比直接调用构造函数稍复杂,但思路是清晰的:获取现有 memberDecls 中的字段列表,将新字段添加至其头部,然后用这个更新后的列表(连同原有的方法列表)创建一个新的 memberDecls 节点。
本节课中我们一起学习了如何利用图形化测试工具提升开发效率,并深入剖析了一个在构建抽象语法树时的复杂语法规则,明确了其设计原因并给出了具体的实现思路。掌握这些内容将帮助你更顺利地完成当前的项目。
009:课程安排与项目说明 📅




在本节课中,我们将了解课程近期的安排,包括第一次考试、项目截止日期以及新项目的启动。我们还会讨论如何利用现有资源进行备考。





考试安排与备考资源 📝




首先提醒大家,下周一是第一次考试。考试将在本教室进行,时间为下午5:30至7:30,共两小时。考试内容涵盖我们到目前为止所学的所有知识,包括LL(1)语法分析,但不包括今天将要讨论的LR语法分析。LR分析的内容将纳入期末考试。



你需要掌握LL语法分析的基础知识,包括:
- 什么是LL分析器。
- 预测集的概念及其计算方法。
- 如何利用预测集进行语法分析,无论是通过分析表还是递归下降分析器。




为了帮助大家备考,我准备了一份模拟试卷及其参考答案。

以下是使用这份模拟试卷的建议:
- 你可以按专题逐个练习。
- 也可以设定一两个小时,像对待真实考试一样完成它。
- 完成练习后,将你的答案与参考答案进行对比,评估自己的掌握情况。



模拟试卷和答案也发布在课程主页的“考试”标签页下,方便大家随时获取。







项目进度与答疑时间 💻



项目3(语法分析器)的截止日期是今天。稍后我会留出时间,解答大家关于这个项目的最后疑问。


此外,许多同学可能会参加第一次考试。你也可以提前询问与考试相关的问题。当然,在考试前的周三,我依然有办公时间。即使在考试当天,你也可以私下找我提问,然后去参加考试。






作业与后续项目 📚

还有一份作业2,主要内容是语法设计问题。这份作业在第一次考试之后才截止。完成这份作业有助于你为考试做准备,因为它涉及语法和LL风格的分析。建议你先完成模拟试卷,核对答案,纠正理解上的偏差,然后再来处理这份作业,这样效果会更好。

最后一项是项目4。就项目进度而言,我们已接近学期尾声。今天我会简要介绍一下这个项目,看看类型检查在其中扮演什么角色。



关于项目4,有一个好消息和一个坏消息:
- 好消息是:在这个项目中,你不需要学习使用任何像JLex或Java CUP这样的新工具。
- 坏消息是:你也没有任何高级工具来帮你处理繁琐的工作。因此,这基本上需要你直接编程实现。



这个项目的难点在于,编程语言有大量的规则需要遵守。如果说指导道德准则只需要十诫,那么CSX(课程中的示例语言)可能需要25或30条“你应当”和“你不应当”的规则。






利用 Piazza 平台 🤝




由于项目3今晚截止,我今天在Piazza上看到了一些最后时刻的提问。在最终完成项目之前,如果你有任何问题,请务必查看Piazza,因为如果你遇到了问题,很可能其他同学也遇到了同样的问题,并且可能已经得到了解答。




我要感谢那些在Piazza上积极参与,特别是回答问题的同学。当我不在线时,或者有时你们因为思考得更深入而能给出比我更好的答案时,你们的帮助非常大。



Piazza是一个双向工具:你既可以提问,也可以通过提供建议来帮助他人。它运行得非常出色。令人惊讶的是,Piazza已经存在相当长一段时间了,并且至今仍然完全免费。我时常在想,他们是如何维持服务器运行、进行软件升级等所有工作的。我想他们可能尝试通过其他方式盈利。





本节课中,我们一起了解了第一次考试的时间、内容和备考方法,明确了项目3的截止日期和新项目4的启动,并强调了利用Piazza平台进行协作学习的重要性。请大家合理安排时间,积极备考和完成项目。
010:期中考试与类型检查项目概述 🎯





在本节课中,我们将学习期中考试的相关安排,并重点介绍课程项目四——类型检查。我们将了解考试的形式、复习材料,并深入探讨类型检查项目的基本概念和实现方法。




期中考试安排与复习 📝


上一节我们介绍了课程的整体进度,本节中我们来看看即将到来的期中考试。




考试将在下周一举行。本次考试与昨天进行的考试类似,但题目不完全相同。昨天参加考试的同学可以将其视为一次有益的练习。




以下是关于考试和复习材料的要点:
- 已经分发了练习题及其答案。如果尚未领取,可以现在获取。
- 考试涵盖的内容包括项目一、项目二以及LL语法分析,但不包括项目三的LALR语法分析。这大约覆盖了课程一半的内容。
- 本周四仍有答疑时间,助教本周也会在岗。但下周是感恩节假期,助教将休假。



关于感恩节假期安排,周三和周五大学放假,助教将休息。如有问题,仍可通过Piazza平台联系。



现在,开放提问时间,任何关于期中考试范围(包括项目一、项目二、语法分析、推导、歧义、语法树等)的问题都可以提出。





项目四:类型检查介绍 🔍


在了解了考试信息后,我们转向今天的核心主题——项目四:类型检查。




关于类型检查,有一个常见的比喻:有好消息也有坏消息。
- 好消息是:你不需要学习像Java CUP/JFlex这样的新工具。
- 坏消息是:你也没有任何高级工具来替你完成部分工作。


如果你查看项目作业,会发现在第一页底部、第二页以及第三页的一半,列出了许多带项目符号的规则条目。与只需“十诫”就能应付的某些阶段不同,我们的语言有大约三四十条“你必须”或“你不得”的规则。



如果你有合作伙伴,可以很方便地分工,每人负责一部分规则条目。正如我们将在后续课程中看到的,这基本上意味着每个特定的抽象语法树节点都可能需要进行一些检查,或者其子树需要被检查。

一旦符号表准备就绪(我们稍后会讨论符号表),你几乎可以按任何顺序实施这些规则。例如,有一条规则要求while循环的控制表达式必须是布尔类型。目前,你的程序没有任何机制强制这一点。实际上,可能会出现明显错误的情况,比如用整数字面量作为循环控制表达式。




在某些语言中,数字可以隐式转换为布尔值(通常0表示false,非0表示true),但在我们的语言中不允许这样做。






类型检查规则与实现思路 ⚙️




上一节我们介绍了类型检查项目的基本情况,本节中我们来看看具体的规则和实现思路。



项目作业中列出的众多规则,每条都对应着语言中一个具体的类型约束。实现类型检查器,本质上就是遍历抽象语法树,并在每个节点处执行相应的规则检查。


例如,对于While语句节点,你需要检查其条件表达式子树的求值结果类型是否为boolean。这通常意味着你需要先递归地检查该表达式子树,并获取其类型,然后与期望的类型进行比较。




符号表在这个过程中至关重要,因为它存储了变量、函数等的类型信息。在检查一个表达式(如变量引用或函数调用)的类型时,你需要查询符号表来获取相关信息。



因此,实现类型检查器的大致步骤是:
- 建立并维护好符号表,包含所有声明的类型信息。
- 深度优先遍历抽象语法树。
- 对于每个节点,根据其种类(如赋值、循环、函数调用等),执行作业中列出的对应规则。
- 在检查表达式节点时,递归地确定其类型,并与上下文要求的类型进行匹配。
- 如果发现类型错误(如将整数赋值给字符串变量),则报告错误信息。



这种方法的好处是模块化清晰,每个节点的检查逻辑相对独立,便于分工和调试。





总结 ✨



本节课中我们一起学习了期中考试的安排和复习重点,并详细介绍了课程项目四——类型检查。



我们了解到:
- 期中考试定于下周一,复习材料已分发,重点覆盖前半学期内容。
- 项目四的核心任务是实现一个类型检查器,无需使用新的语法分析工具,但需要手动实现大量类型规则。
- 实现的关键在于结合符号表的信息,递归遍历抽象语法树,并在每个节点实施具体的类型约束规则。



通过完成这个项目,你将深入理解静态类型检查的工作原理,这是编译器构建中的一个核心环节。
011:类型检查与重载处理
在本节课中,我们将要学习类型检查中一个较为复杂的方面:如何处理重载。我们将探讨如何在符号表中管理具有相同名称但参数不同的方法,并理解相关的辅助信息存储需求。
上一节我们介绍了符号表的基本概念,本节中我们来看看如何处理方法重载这一特殊情况。

重载的处理方法
当程序中存在多个名称相同但参数列表不同的方法时,编译器需要一种机制来区分它们。这类似于字典中一个单词有多个释义,需要根据上下文来确定具体含义。
以下是处理重载的两种主要思路:
- 单一符号表条目链式存储:为方法名创建一个符号表条目,然后将所有重载版本的方法声明以链表形式链接到该条目下。当遇到一个方法调用时,首先找到方法名对应的条目,然后根据调用时提供的参数数量和类型,遍历链表以匹配正确的声明。
- 存储辅助参数信息:无论是否存在重载,在符号表中为每个方法存储其参数的类型和数量信息都是必要的。这使得在类型检查阶段,可以验证方法调用处的参数是否与声明的参数列表匹配。
符号表中的辅助信息
除了处理重载,符号表还需要存储其他关键信息以确保程序语义正确。
以下是两种需要额外存储信息的情况:
- 方法声明:对于每个方法,符号表条目不仅需要记录其返回类型和种类(如“方法”),还必须存储其参数的类型和数量列表。这用于在调用点进行参数匹配检查。
- 数组声明:对于数组声明,符号表条目需要存储数组的大小(如果语言要求在编译时已知)。例如,声明一个整数数组时,除了类型和种类(“数组”),还需记录其大小。这在后续进行数组赋值操作时,用于检查两个数组是否具有相同的大小(如果语言有此要求)。
课程总结

本节课中我们一起学习了类型检查中处理重载的方法。我们了解到,可以通过在符号表中为方法名建立单一条目并链式存储不同重载版本来管理重载。同时,为了进行有效的类型检查,符号表必须为方法和数组等结构存储必要的辅助信息,如参数列表和数组大小。这些机制共同确保了程序在编译时能够被正确地分析和验证。
012:课程更新与项目安排 📅
在本节课中,我们将回顾期中考试情况,并重点讨论课程项目的后续安排与截止日期调整。本节内容旨在确保所有学生明确当前的学习任务与时间规划。
考试情况回顾
上一节我们介绍了课程的整体进度,本节中我们来看看期中考试的具体情况。
考试最高分为97分,有少数学生达到了这个分数。计算First集和Follow集这类题目很容易因疏忽细节而失分,我自己也常犯此类错误。这不是人们普遍擅长的领域。
虽然扣分不多,但如果答案中出现了本不该存在或遗漏了本该存在的内容,就必须扣分。因此,在这种题目上丢失一两分很容易。
以下是分数段分析:
- 90分及以上:没有问题。
- 80分及以上:没有问题。
- 70分及以上:足够好。
- 60分及以下:本应做得更好,尤其是50分和40分段的学生。
课程项目的核心目标。考试的目的是测试那些项目无法涵盖的内容,例如正则表达式理论和语法分析理论。测试你是否掌握了这些理论知识是有益的。
项目四截止日期调整
接下来,我们关注一个重要的课程安排变动:项目四的截止日期延长。
我收到了大量关于延长项目四截止日期的请求。在像上周感恩节这样的假期周安排任务总是存在问题。对一些人来说,这是额外的追赶时间。我本人则在假期期间批改试卷。
但你们中的许多人要么出行,要么甚至有客人来访。这通常意味着工作量比平时更大而非更少。因此,在那段时间进行项目工作可能确实不容易。
所以我提议:我们不能将截止日期推迟太久,但可以为所有需要的人适当延后。如果你已经提交了,那很好。新的截止日期是本周五午夜或晚上11:59,无需特别申请。
如果你需要更多时间,请通过邮件或联系助教告诉我你希望的目标日期。我认为在这种情况下,额外几天时间能解决很多问题。
我会自动处理这项延期安排。
材料分发与后续步骤
现在,我们来看看本节课需要分发的材料以及后续步骤。
我的下一步是完成第一次期中考试的相关工作。在少数情况下,我忘记带来批改好的试卷。我没有答案页。这属于那种我明明把它放在桌上以免忘记,却又把键盘压在了上面的情况。正如人们所说,最显眼的地方往往是最好的藏匿之处。我看着桌子,只看到一个空桌面上放着键盘,没能看到下面的试卷。
首先,让我处理这两次考试。对于第二次考试,你们将收到批改好的纸质试卷,同时也会有一份答案页,以便你们理解我期望的答案。
另外,这些成绩是综合的,包含了第一次和第二次考试的成绩。两次考试的分数大致相同,难度也基本相当。
现在开始分发第一次考试的试卷和答案页。以下是部分学生名单:Ron Denenberg、Iran、Dust and Yellow、Caitlin Clingger、DX Chen、William Maybebury、Zueiing、Josh Gordon。上周已拿回试卷但未收到答案页的同学,如果需要可以领取。这些纸张如果没有其他用途,很适合用来垫猫砂盆。猫看一眼就会决定是时候使用了。
第一次考试和作业相关事宜到此结束。
现在分发第二次考试的批改试卷和答案页。这些材料没有特定顺序,我不是按字母顺序或其他方式整理的,只是按照收到的顺序批改。
以下是部分学生名单:Winslow Cho、Gregory Blair、Scott Wang、Kit O Chung、Ash exhum、Benjamin Ziegler、David Dailycall。
课程核心:完成编译器项目
在回顾了考试和调整了项目截止日期后,我们的重点将回归到课程的核心任务上。
我们正在进行的项目是关键目标。我今天会重点讨论它,并且我要分发的许多材料都与项目五以及完成编译器相关。请务必以此为目标。
本节课中,我们一起回顾了期中考试的整体表现与分数分布,宣布了项目四截止日期延长至本周五的决定,并分发了两次考试的试卷与答案。我们的核心任务仍然是专注于编译器项目的完成。请合理安排时间,确保项目进度。
013:代码生成与寄存器分配 🧠
在本节课中,我们将探讨编译器后端的一个核心问题:代码生成过程中的寄存器分配。我们将分析一个具体的场景,理解为何简单的双寄存器策略可能不足,并学习如何递归地计算一个表达式树所需的最小寄存器数量。
上一节我们讨论了语法分析和相关算法。本节中,我们来看看代码生成阶段的一个具体挑战。

许多代码生成器面临一个关键问题:它们不能简单地将操作数压入一个操作数栈。相反,目标机器要求使用寄存器来存放中间值。
因此,在编译的早期,人们提出了一个问题:对于一个给定的表达式,最少需要多少个寄存器?对于二元运算符,显然至少需要两个寄存器。
以下是一个简单的算法思路:告知一个节点(可能是树或叶子)将结果放入哪个寄存器(例如寄存器0或1),然后让它生成相应的代码。
以下是该算法的一个示例:
// 例如表达式 a - b
LOAD a, R0
LOAD b, R1
SUB R1, R0 // 结果留在 R0
在这个例子中,双寄存器策略运作良好。
然而,对于更复杂的表达式,两个寄存器可能就不够用了。如果情况确实如此,我们就需要一种方法,在生成代码之前就能预测任意表达式树所需的最小寄存器数量。
规则如下:
- 不能改变表达式树的结构。
- 不能改变运算符。
- 可以自由选择子表达式的求值顺序(不一定是左到右)。
我们的目标是推导出一个公式或算法,给定一棵表达式树,就能计算出其所需的最小寄存器数。显然,对于任何非叶子节点,至少需要两个寄存器。
以下是一些需要考虑的关键点:
- 如果多个叶子节点是相同的(即同一个变量被多次使用),问题会变得更复杂,因为你可能加载一次后重复使用。但本次讨论的问题已做了简化。
- 表达式树天然适合递归处理。因此,解决方案很可能是一个递归算法。
- 在递归过程中,如果左右子树的求值顺序会影响寄存器需求,我们应选择需求更少的那种顺序。
关于解决方案的一个提示:我期望的答案不会出现分数形式的寄存器数量。虽然数学和物理中存在诸如虚数、量子隧穿等看似“不可能”的概念,但在这个具体问题中,寄存器数量是整数。
本节课中我们一起学习了代码生成中寄存器分配的基本问题。我们了解到,简单的双寄存器策略对于复杂表达式可能失效,因此需要一种能够预先计算最小寄存器需求的递归算法。掌握这一概念对于理解编译器如何高效利用有限硬件资源至关重要。
014:期末复习与条件表达式详解 🎓





在本节课中,我们将学习如何为期末考试做准备,并深入探讨条件表达式(Conditional Expression)的概念及其实现。课程将涵盖实践考试的使用、条件表达式的核心思想,以及如何将其应用于短路求值(Short-Circuit Evaluation)等场景。








实践考试与复习安排 📝


首先,我承诺过会提供一份带答案的实践考试试卷。我已经将其分发,如果你已经拿到或自行打印了,可以开始使用。




以下是关于实践考试的一些说明:
- 试卷和答案分页印刷,以避免你提前偷看答案。
- 你可以根据自己的复习计划,以任何方式使用这份试卷。
- 这些题目来自往年的真实考试,能让你了解我可能会出的题型。

今天作为最后一讲,我的计划是先讲解一些新内容,时间大约不到一小时。之后我们会照常休息,休息后开放提问环节,讨论期末考试的主题、范围和时间限制等。








条件表达式与短路求值 ⚡

上一节我们介绍了实践考试。本节中,我们来看看一个被频繁问到的问题:如何实现条件与(Conditional AND)和条件或(Conditional OR)操作?实际上,实践考试的第一题就是关于这个问题的答案。




条件表达式(if-then-else)的核心是:计算一个布尔表达式,并根据其结果选择执行两个子表达式中的一个(then表达式或else表达式)。短路求值的与/或操作只是条件表达式的一种替代形式。
其工作原理如下:
- 对于条件与(
&&):先计算左侧项。如果结果为false,则整个表达式立即返回false,无需计算右侧项。因为false && anything的结果总是false。 - 对于条件或(
||):先计算左侧项。如果结果为true,则整个表达式立即返回true,无需计算右侧项。因为true || anything的结果总是true。





这种“短路”特性初看可能像个小技巧,但实际上非常有用。








短路求值的应用场景 🛡️


理解了条件表达式的原理后,我们来看看短路求值在实际编程中的两个典型应用场景。




以下是短路求值的常见用途:




- 防止除零错误:在除法运算前,先检查除数是否为零。例如,表达式可以写为:
(divisor != 0) ? (dividend / divisor) : 0。如果除数为零,则直接返回一个默认值(如0),避免程序崩溃。 - 安全访问可能为空的引用:在访问对象的字段或方法前,先检查引用是否为
null。例如:(ref != null) ? ref.field : defaultValue。这在处理链表(如检查next字段是否为null)或符号表等数据结构时非常常见。
如果你已经掌握了如何实现条件语句(if语句),那么实现条件表达式并不会太难。在编写答案时,我基本上复制了原来的条件语句代码,然后将其从语句形式编辑为表达式形式。








项目与考试相关答疑 🔧




在进入总结之前,我们还需要处理一些关于项目和考试的技术细节与安排。

关于项目中的输入文件,需要注意以下几点:
build.xml是一个XML格式的文件,某些浏览器可能会将其渲染成树状结构视图。如果显示不正常,请直接下载文件并用文本编辑器查看。- 输入文件应命名为
[你的程序名]_input。这样可以为不同程序共存多个输入文件。 - 重要提示:如果输入文件中需要读取整数,请确保每个数字后面都有一个空格或换行符。这是因为输入读取器需要看到一个非数字字符才能确定当前数字已结束。如果文件末尾没有分隔符,读取器会尝试读取超出文件范围的内容并抛出异常。


关于期末考试安排:
- 考试将分两场进行,第一场在本周五。
- 本周三(如果天气允许)和周四会有助教和我的常规答疑时间。
- 第二场考试安排在下周三。
- 成绩反馈的速度将取决于助教批改的进度。








总结 📚

本节课中我们一起学习了期末考试的复习方法,并深入探讨了条件表达式与短路求值的核心概念。我们了解到:
- 短路求值的
&&和||操作本质上是条件表达式的特例,它通过只计算必要的子表达式来提升效率。 - 这一特性在防止除零错误和安全访问空引用等场景中非常实用。
- 在实现上,可以基于已有的条件语句代码进行修改。
- 最后,我们回顾了项目输入文件的注意事项和期末考试的具体安排。




希望本讲内容能帮助你更好地准备期末考试。

浙公网安备 33010602011771号