零基础编译原理入门:从词法分析到最小解释器实践
简介《编译原理学习指导》是一份面向计算机专业本科生、考研复习者及自学编译原理的开发者整理的doc文档重点回应课程概念抽象、算法门槛高、教材选择困难等问题。内容围绕词法分析、语法分析LL、递归下降与LR、语义分析、运行时环境、中间代码生成及代码优化等核心阶段展开并结合龙书、《现代编译程序设计》《编译原理及实践》三本经典教材的特点给出阅读建议还涉及正则表达式与自动机、Tiny C实践、Lex/Yacc工具等学习线索便于读者建立从理论到实现的整体框架。压缩包仅含1个doc文件大小约34KB轻量便携适合作为课程复习提纲和教材选读指南。目前已有153人学习可帮助读者快速梳理知识脉络、明确重点难点并规划后续实践方向。1. 先弄明白一件事编译原理到底在讲什么很多人第一次听到「编译原理」这四个字第一反应是「我又不去造编译器学它干什么」第二反应是「听起来就很难是不是得先把一堆数学啃完」。这两个反应都很正常我第一次翻开教材时也是这么想的。用一句大白话说清楚编译原理研究的就是你写的那几行给人看的代码是怎么一步步变成机器真正能执行的指令的。它把这件事拆成词法分析、语法分析、语义分析、中间代码、代码生成、代码优化几个环节每个环节只解决一个小问题串起来就是一个完整的编译器。学它的人不一定要去写编译器但每一个写代码的人都站在它的下游。有人把代码当成黑匣子扔进去出来结果就完事有人能顺着这条流水线往下看一眼报错信息在说什么、这段代码为什么慢心里就有底了。2. 为什么零基础也值得学先看清这条流水线2.1 编译器是条流水线七个环节一口气看懂先别急着记术语。你可以把编译器想成一家翻译公司客户交来一份手写稿源代码公司要把稿子翻成对方国家的语言机器指令。公司里不是一个人从头翻到尾而是分成几道工序每道工序只干一件事干完交给下一道。编译原理讲的就是这几道工序各自要解决的问题以及用什么标准方法去解决。阶段它实际在干什么用大白话讲词法分析把字符流切成一个个有意义的词把一整段话拆成一个个单词语法分析按语法规则把词组织成树检查句子成分对不对、谁修饰谁语义分析检查类型、作用域是否合理检查这句话意思讲不讲得通运行时环境变量放哪、函数怎么调用、参数怎么传给演员安排后台位置和上场顺序中间代码换成一种和具体机器无关的表示先翻成一种通用的中间语言代码生成把中间表示变成目标机器的指令最后翻成对方母语代码优化让结果更短、更快、更省润色译文删掉啰嗦的地方这张表你不用背。真正要带走的是两个印象第一编译器不是一个神秘的整体它是一条可以拆开看的流水线第二越靠前的工序教材讲得越细本科教学的重点基本压在词法分析和语法分析这两道上后面的代码优化是个无底洞单独开一学期也讲不完入门阶段知道它在那就行。2.2 学了不写编译器也不亏三样会跟着你走的能力第一个能力是看懂报错。写过代码的人都遇到过「明明语法没问题它就是报错」的时刻。学完语法分析和语义分析这两块你会知道报错其实分两类一类是「这句话的结构就不合法」另一类是「结构没错但意思不对比如类型对不上」。分清这两类排查速度会明显不一样。第二个是树形思维。教材最后都把源代码组织成「树」这种结构而不是数组或者链表原因很实在树有很强的递归性你把树上任何一个节点摘下来它本身还是一棵完整的树。而程序语言里函数套函数、循环套循环、条件套条件这种嵌套关系用树来表达最自然。这个思维一旦建立起来你再看配置文件、看 JSON、看 HTML 结构会发现自己自动在脑子里画树。第三个是把「不规则的文本」变成「好处理的数据结构」这套方法。命令行里敲的那串命令是怎么被解析的日志文件怎么按格式抽出字段一段配置文本怎么变成程序里的对象背后都是同一套思路先切词再按规则组装。这套东西不挑语言也不挑场景是能反复用的。2.3 零基础先补什么两样就够别贪多很多人被劝退不是因为课程难而是因为在开始之前先去补了一堆「前置知识」补着补着热情就没了。入门阶段真正需要的其实只有两样。第一样是能用一门编程语言写出几十行能跑的程序。要求不高会定义函数、会写循环、会用数组和字符串、知道怎么打印结果。编译原理的教材里到处都是算法光看懂文字没用你得能把它落成代码跑一遍。第二样是知道「树」和「栈」大概是什么。树就是有父子层级的结构栈就是后进先出的一摞盘子。这两个概念在数据结构课里会讲但就算你没系统学过花半小时看看图也能明白个七八成剩下的在做实验的过程中自然会补上。提示形式语言、自动机这些理论教材里会讲但它们是跟着词法分析一起出现的不需要提前单独啃一遍。先看怎么用再回头补为什么顺序反过来很容易卡住。我第一次学的时候犯过一个错觉得理论没搞透就不配动手结果在第一章停留了两周什么也没跑起来。后来换了个顺序先跟着书写了个能切词的小程序看到屏幕上真的吐出一串结果才明白前面那些定义是在描述什么。3. 三本经典教材怎么选别一上来就啃最厚的那本3.1 三本书脾气不同先对上你的目标市面上被反复推荐的三本经典教材讲的其实是同一件事但侧重完全不同选错了会很痛苦。教材类型最大特点适合谁什么时候读业界俗称「龙书」的那本原理经典开篇就用一个完整的小例子把整个流程走一遍先让你心里有底再逐章展开理论想系统打基础、准备考试、想把原理搞明白的人入门第一本但只读贯穿例子和词法、语法两章主打现代编译器设计实践的那本给不少实际代码讲了很多「现代」的东西比如自动内存管理这类传统教材里没有的内容已经知道流程、想动手做一个像样的编译器的人第二本边做边查带着你写一个教学用小型编译器的那本讲解细致、点到为止重点是边讲原理边实践最后你能攒出一个完整的小编译器对词法语法工具讲得很详细完全零基础、想「读完就能有一个作品」的人可以和第一本并行作为动手主线如果你只想先买一本我的建议是以「龙书」作为主线建立框架因为它开篇那个例子能让你很快知道这些理论是干什么用的这恰恰是很多教材缺的东西——不少教材写得很严谨但看了半天不知道里面的东西能用在哪。另外两本一本当工具书一本当动手手册。3.2 建议的顺序先看全貌再动手做小的新手最容易犯的错是从第一页第一个字开始一行一行往后读读到中间读不动了书签就永远停在那。正确的打开方式分三步。第一步先把「全貌」拿到手。翻到那个贯穿全书的小例子别管细节跟着它走一遍一段源代码进去经过几道工序最后出来什么东西。看不懂的地方做个记号直接往下走。这一步的目标不是学会是知道地图长什么样。第二步回过头认真啃词法分析。这一章相对简单而且能立刻动手成就感来得快。重点是理解它为什么要引入正则表达式和自动机这套理论——不是为了炫技而是为了让「写一个切词程序」从手工作坊变成有标准流程的工程。第三步再进语法分析。先学自顶向下的那一类因为它直观、实践简单自己手写一个不难自底向上的那类算法理解到「知道它在干什么」就够了实际工程里这类分析器基本都是用工具生成的很少需要你从零手写。3.3 读书节奏一章原理配一个最小实验我的血泪经验是光看书一周之后基本什么都不剩。有效的节奏是每读完一章立刻配一个三十到一百行的小实验实验不用完整只验证这一章的一个点。比如读完词法分析就写个切词程序把一行表达式切成若干个小块读完语法分析的自顶向下部分就写个能算加减乘除的递归下降小计算器读完语义分析就给前面的计算器加一条规则整数和浮点数不能直接混着赋值混了就报错。每个实验都很小但串起来就是一条完整的链子。注意实验的目标是「跑通并且能解释每一行」不是「功能多」。一个能跑但你讲不清原理的大程序对入门的帮助还不如一个三十行但每行都能说明白的小程序。英文版和中文版的选择上不用纠结。三本经典的中文译本质量都不错入门阶段理解理论的实质比抠字眼重要得多等你把主干跑通了再去看原版补细节完全来得及。4. 第一个能跑的例子三十行写一个词法分析器4.1 先想清楚输入长什么样词法分析是整条流水线的第一道工序任务很单纯把一长串字符切成一个个有意义的「词」每个词贴上一个种类标签。就像你看到total price * (count 2.5)脑子里会自动分出「这是个变量名、这是个等号、这是个左括号、这是个数字」词法分析器做的就是把这件你下意识在做的事写成程序。先约定一个极小的语言只允许变量名、数字、四则运算符、等号和圆括号。这个范围小到有点寒酸但足够把词法分析的核心套路走完。后面你会发现真实语言的词法分析器不过是这张规则表长了很多倍而已。4.2 用「正则思路」切词代码和逐行解释核心思路只有一句话给每一类词写一条匹配规则然后从字符串开头一路扫到结尾每次看当前位置能匹配上哪条规则匹配上就切下来匹配不上就报错。import re # 规则表左边是词的种类右边是匹配这一类的正则 RULES [ (NUMBER, r\d(?:\.\d)?), # 整数或小数比如 42、3.14 (ID, r[A-Za-z_][A-Za-z0-9_]*), # 变量名字母或下划线开头 (OP, r[\-*/]), # 单字符运算符 (LPAREN, r\(), (RPAREN, r\)), (SKIP, r[ \t\n]), # 空白字符切下来直接扔掉 ] # 把整张规则表拼成一个大正则每个分支起个名字方便回头知道命中的是哪一类 PATTERN re.compile(|.join(f(?P{name}{rule}) for name, rule in RULES)) def tokenize(src): pos 0 # 当前扫描到源码的哪个位置 tokens [] # 用来收集切出来的词 while pos len(src): m PATTERN.match(src, pos) # 从 pos 这个位置开始尝试匹配 if m is None: # 一条规则都没匹配上说明遇到了非法字符 raise SyntaxError(f第 {pos} 个字符不认识: {src[pos]!r}) kind m.lastgroup # 命中的是哪一类 if kind ! SKIP: # 空白不产生词跳过 tokens.append((kind, m.group(), pos)) pos m.end() # 从这次匹配结束的位置继续往后扫 return tokens if __name__ __main__: source total price * (count 2.5) for tok in tokenize(source): print(tok)这段代码里有几个地方值得停下来看一眼。RULES是一张表每一行是「种类 正则」。这种写法本身就是编译原理里反复出现的思想把规则和数据分开写程序只负责执行规则。以后你想支持字符串字面量只需要往表里加一行主循环一个字都不用改。(?Pname...)是给正则的一个分支起名字。因为所有规则被拼成了一条大正则匹配成功之后程序需要知道「刚才命中的到底是哪一条」靠的就是这个名字。主循环里那三行是关键从当前位置尝试匹配、记录结果、把位置推到匹配结束处。整个词法分析器本质就是这三步的循环直到扫完最后一个字符。pos这个变量记录扫描进度m.end()返回本次匹配的结束位置少了它就会死循环这是新手最容易翻车的地方之一。4.3 跑起来预期输出和出错时看什么把上面这段存成一个.py文件在命令行里运行它。如果你的环境里装了 Python直接敲文件名就行没装的话去官网下个安装包安装时记得勾上把 Python 加进系统路径的那个选项不然命令行会提示找不到命令。跑通之后屏幕上应该出现这样的结果(ID, total, 0) (OP, , 6) (ID, price, 8) (OP, *, 14) (LPAREN, (, 16) (ID, count, 17) (OP, , 23) (NUMBER, 2.5, 25) (RPAREN, ), 28)每一行是一个三元组种类、切下来的原文、它在原字符串里的起始位置。第三个数字现在看着没用等语法分析阶段要报「第几行第几列出错」时你就知道它有多重要了。如果程序没按预期输出按这个顺序查三件事。第一报「第 N 个字符不认识」说明你的规则表漏掉了某种字符看看报错位置上那个字符属于哪一类是空格没写进跳过规则还是出现了中文标点。第二程序卡住不动多半是位置没有往前推检查pos有没有被正确更新。第三切出来的词比你预想的多或者少去核对规则表的先后顺序——正则的匹配是从上往下试的把「数字」放在「标识符」前面还是后面结果会不一样。4.4 改三个地方让它更像真的词法分析器能跑通只是起点接下来做三次小改动你会对「工业级」这三个字有更具体的感受。第一个改动加关键字。把if、else、while这类词单独识别出来。做法很朴素先按标识符切出来再看它的文本是不是在关键字表里是就把种类改成关键字。就多了几行判断但这正是真实编译器识别关键字的常规做法。第二个改动加错误恢复。现在遇到非法字符直接抛异常退出真实的编译器不会这么脆它会跳过这个字符记一条错误然后接着往下扫。你会发现「报错」和「继续干活」这两件事需要分开设计这个思路在后面的语法分析里还会再遇到一次。第三个改动把位置信息从字符下标改成「行号 列号」。改完之后你的报错信息就能写成「第 3 行第 12 列附近有个不认识的字符」这已经和专业工具的输出格式很接近了。三个改动都不难加起来不到五十行。做完之后你会对一句话有实感所谓编译原理的理论落到代码上就是这些具体到有点琐碎的决定。5. 新手最容易踩的五个坑5.1 卡在自底向上那类算法上然后整个放弃现象很典型前面词法分析学得挺顺一到语法分析的自底向上算法看到那一堆状态和冲突反复看几遍都理不清然后开始怀疑自己不适合学这个书一合就再没打开过。原因不在你。这类算法本身推导就绕而且教材为了严谨往往把完整构造过程一步步写出来读起来像在解一道很长的数学题。更关键的是它在实际工作里的使用方式和你想象的不一样真实的语法分析器基本都是用工具根据语法规则自动生成的很少需要人从零手写这套算法。解决办法是把要求降下来这部分理解到「它在干什么、为什么能处理比自顶向下更复杂的语法」就够了不必强求自己能默写构造过程。省下来的时间拿去做自顶向下的递归下降分析器那个东西简单、直观而且你能独立写出来成就感完全不同。5.2 只看书不动手合上书什么都想不起来现象是书读得挺快一章接一章笔记也做了不少但过两周再问「词法分析大概怎么做」脑子里只剩几个名词具体流程一片空白。原因是编译原理是一门高度程序化的学问它的每个概念都对应一段可以跑起来的算法。只读文字你记住的是「这个词出现过」而不是「这件事怎么做」。人脑对流程的记忆极度依赖亲手操作光看菜谱和真下过厨完全是两个状态。解决办法是给自己定一条硬规矩每读完一章必须有一个能跑的东西哪怕只有二十行。跑完之后合上书用自己的话把这个实验的每一步讲一遍讲不顺的地方就是没懂的地方回去补那一小段。5.3 非要从零手写词法分析器进度停在第一章现象是学到词法分析觉得「既然学了就得全部自己写」于是开始死磕怎么把状态机一行行码出来写了三百行还一堆 bug两周过去进度条没动。原因是把「学原理」和「造工具」混成了一件事。切词这件事工具几分钟就能生成一个能用的版本你手写的那个大概率还没它稳。入门阶段的时间应该花在理解流程上不是花在重复造轮子上。解决办法很简单词法分析这一层优先用现成的生成工具把精力留给「规则怎么写」这件事本身。自己手写一个最小版本作为理解是可以的但设在三十到五十行跑通就收不要陷进去。语法分析里的递归下降是例外那个建议亲手写因为它简单且能让你真正理解「程序怎么按语法结构走」。5.4 把原理课和技术课当成同一门来学现象是学着学着开始困惑书上讲的这些理论和我真正想让一段代码跑起来之间好像隔着一层想做一个能跑的小语言又不知道该补什么。原因是这两门课关注的东西确实不同。原理课关注「编译器这类东西背后的基本理论是什么」技术课关注「实际编写过程中用到哪些技术和工具」。前者求懂后者求成。入门阶段如果拿技术课的标准要求原理课会觉得处处不够用反过来拿原理课的标准要求技术课又会觉得处处不严谨。解决办法是先明确自己这一阶段的目标。想打基础、应付考试、把概念理顺就跟紧原理主线想做出一个能跑的小东西就在主线之外单独开一条实践线两条线各走各的别互相指责。5.5 一上手就想生成机器码被底层细节劝退现象是刚学完前几章热血上头直接立了个「写一个真正的编译器」的目标结果卡在寄存器怎么分配、指令怎么选这些地方一堆从没接触过的底层知识压过来热情迅速耗尽。原因是从中间表示直接跳到机器码中间隔着一大片运行时环境和目标机器相关的知识。这块内容的复杂度不比其他部分低只是教材通常讲得比较简略。解决办法是换个顺序先做解释执行把前面分析出来的结构直接遍历求值跳过生成机器码这一步。这样你依然拥有一个完整可跑的语言实现但绕开了最硬的那块骨头。等你把前面整条链路都摸熟了再回头补代码生成那时候你已经知道每一步在干什么学起来不会那么绝望。6. 下一步学什么一条能自己走下去的路入门阶段最怕的是「学完之后不知道往哪走」。给你一条四到六周的路每周都有明确的验收标准做到了就往下走没做到就停一周补。阶段重点做什么验收标准第 1 周通读教材里那个贯穿全书的小例子画出从源码到结果的完整流程图能对着空白纸把这几个环节按顺序讲出来每个环节说一句它在干什么第 2 周词法分析用工具生成一个切词器再手写一个三十行的版本对比两个版本对同一段输入给出相同结果能解释规则表的顺序为什么重要第 3 周语法分析手写一个递归下降的表达式求值器能算带括号和优先级的四则运算能对写错的表达式给出位置和原因第 4 周语义分析给求值器加上变量声明和类型检查给浮点变量赋整数值不报错给整数变量赋浮点值报错报错信息带位置第 5-6 周解释执行把前面几层串起来做成一个能跑多行程序的小解释器能读入一个文本文件逐行执行输出结果出错时指出第几行走完这条线你已经把编译器最主要的那几道工序亲手过了一遍。这时候再回去看后面那些章节就不会觉得是在看天书而是在看「我这套东西还能怎么改得更好」。下面这几个方向按难度从低到高排任选一个继续深挖都行。第一个是补工具链把那个切词工具和语法分析工具的规则写熟练这是实际工作中最常用的一块。第二个是补运行时环境那一章它会把你平常写代码时经常做却很少深究的事摊开讲变量到底存在哪、函数调用时参数怎么传、内存什么时候分配什么时候回收。这部分的意义其实超出编译本身因为它把一段程序从写出来到跑起来的全过程摆在你面前。第三个才是代码生成和优化等你前面都稳了再碰。还有一个容易被忽略的练习找一个现成的、体量很小的教学用编译器源码通读一遍。选它的标准不是功能强而是代码短、注释清楚、结构完整。读别人的完整实现能补上你自己写的时候跳过的那些「工程细节」比如错误怎么收集、符号表怎么组织、各模块之间怎么传数据。这一步的收获往往比自己再闷头写一个更大。我自己的习惯是每次开新一章之前先把上一章那个小实验重跑一遍跑通了再往下。听着有点笨但这套流程帮我避开了好几次「以为懂了其实没懂」的情况——代码不会骗人它跑不出来就是没懂。希望这些能帮你少走点弯路早点把第一个小程序跑起来。本文还有配套的精品资源点击获取