我,子牙老师,一个手写过操作系统、编程语言的硬核男人
让我用一篇文章带你速通编译原理
编译原理是什么?编译原理是所有编程语言得以诞生,背后的理论依据
任何编程语言,从运行,到看到结果,都要经历这些阶段
理解了这张图中的每个名词、每个阶段,你就能看懂任何编程语言的底层实现
词法分析
先从词法分析开始。它的输入是程序代码,输出是记号流(token)
如果你想查看Python代码经词法分析后生成的token
对应代码
其他编程语言你如果想查看,自行面向AI研究
词法分析器
在所有编程语言中,负责词法分析的模块叫词法分析器。
如果你想得到词法分析器,有两种方式:自实行、使用词法分析工具生成
先说词法分析工具,开源的还蛮多的
我们这套课程使用的是flex,它的工作流程差不多是这样
细节在后面的课程中再细讲,当前你理解到这个程度即可
绝知此事要躬行
诚然,你可以使用开源的词法分析工具flex生成词法分析器,在此基础上开发编程语言
但是在用的过程中,脑海中一直会有一个声音:它是如何实现的
而且遇上问题,无从下手,因为你不知道它的底层实现
因为理解得不深,还会出现一个问题就是:有时候想实现一个功能但是不知道怎么用它实现
为此,写一个玩玩吧(我觉得任何底层,都值得写一遍,而且只需要写一遍)
很多人学习是为了学习而学习
比如学《编译原理》,直接去啃书。一点概念都没有,怎么看得懂呢
我喜欢先动手干,把书当成工具,遇到问题不知道如何解决,在书中找答案
就写出来了,写出来再后头看理论,就很容易理解
比如词法分析中最难理解的状态机、DFA、NFA、正则引擎、lookahead、回溯、错误机制、代码追踪、缓冲区、最长匹配、优先级、关键字识别……你写的时候就感觉:不就应该这样吗?
后面的视频中会手把手教你如何从零写一个词法分析器,边写边解释这些名词
举个例子
比如把这段代码喂给词法分析器
词法分析器(lexer 或 tokenizer)会将其分解为一系列 token。以下是按顺序产生的 token 列表:
if
— 关键字 (IF
)(
— 左括号 (LPAR
)1
— 整数字面量 (NUMBER
))
— 右括号 (RPAR
):
— 冒号 (COLON
)print
— 标识符(在 Python 3 中是内建函数,不是关键字) (NAME
)(
— 左括号 (LPAR
)2
— 整数字面量 (NUMBER
))
— 右括号 (RPAR
)- 换行符 — (
NEWLINE
) - 文件结束 — (
ENDMARKER
)
语法分析
词法分析器解析程序代码为token,然后丢给语法分析器
语法分析器的输入是token,输出是抽象语法树,即AST
比如代码
生成的抽象语法树长这样
如果你想得到语法分析器,也是两种方式:1、自实行;2、使用语法分析器生成功能生成
主流的生成工具有这些
我们这套课程使用的是bison
当然,为了让你彻底玩明白语法分析,会带你手写实现一个语法分析器。这个词法分析器,可以与我们自实行的词法分析器配合使用,也可以与flex生成的词法分析器配合使用。语法分析阶段涉及的理论如:上下文无关文法、递归向下分析、LR分析、错误处理……会在手写的过程中详细讲解
抽象语法树AST
后面的所有阶段,都是围绕AST工作的
AST也有开源项目可用
AST是属于手写编程语言的核心了,这个我是自己纯手工打造的。这个还不自己写,那手写编程语言就没有意义了
课程中,我会从零带你手写一个AST,带你彻底哪些AST。
如果你从来没有玩过树这种数据结构,那你通过玩AST,对于你后面去玩数据库领域常用的B tree、B+ tree,算是打开了一扇门
语义分析
语义分析是编译器前端中的关键阶段,位于语法分析之后、IR(中间表示)生成之前。其职责是确保源程序在语义上是合理且一致的。下面是语义分析的完整职责清单,分为核心职责与可选扩展:
✅ 语义分析的完整职责
1. 符号解析(Symbol Resolution)
- 建立并维护符号表(Symbol Table)
- 确定标识符(变量名、函数名等)引用的定义
- 检查变量是否已声明
- 处理作用域规则(全局、局部、嵌套作用域)
2. 类型检查(Type Checking)
- 表达式左右操作数类型是否匹配(如
int + float
) - 函数参数与返回值类型是否匹配
- 条件表达式必须为布尔类型(如
if
,while
的条件) - 隐式类型转换是否合法(如
int → float
)
3. 作用域和生命周期检查
- 名字遮蔽(shadowing)检查
- 检查是否访问了超出作用域的变量
- 检查重复定义(如重复变量名)
4. 控制流语义验证
return
、break
、continue
是否出现在合法位置- 所有路径是否都返回(对有返回值的函数)
- 检查不可达代码
5. 函数/方法调用检查
- 参数个数、顺序、类型是否匹配函数定义
- 是否调用未声明或未定义的函数
6. 左值(L-Value)/右值(R-Value)检查
- 赋值语句左边必须是变量(左值)
- 常量不能被赋值
7. 常量表达式求值(可选)
- 可在语义分析阶段做初步常量折叠(Constant Folding)
- 用于宏、数组大小、switch 标签等必须为常量的语法
8. 类型推导(可选)
- 根据上下文推导表达式、变量、函数返回类型(如
auto
或var
)
9. AST 注解(Annotation)
- 给 AST 节点添加类型、引用的符号等信息
- 为后续阶段(如优化、代码生成)提供必要数据
10. 语言特性的语义规则
-
如类的继承检查、重载规则、多态一致性(面向对象语言)
-
模板/泛型约束检查
-
模块依赖检查(import/export)
中间代码
完成语义分析后,就代表代码是没有语法上的问题的,可以正常运行,就需要生成中间代码,丢给虚拟机去运行。
你是不是想问:如果是不需要虚拟机运行的编程语言,比如C、C++,也会生成中间代码吗?是的,也会生成。这个生成中间代码是为了生成后续的机器码做优化用的,与依赖虚拟机运行的中间代码是不一样的
中间代码是由中间代码生成器生成的。中间代码生成器有这些类型
我们所熟知的Java、Python,都是栈式IR
比如Python代码
生成AST
生成栈式IR
是不是这样学习编译原理,理解起来容易很多
课程《手写编程语言》二期,我会教你编写中间代码生成器,将我们自实行的AST,生成Python虚拟机可用的中间代码,让我们自实行的虚拟机,与Python的虚拟机,实现互通。你觉得做起来难不难?
机器码
如果是Java、Python,就没有这个阶段了。比如Java的.class文件,就是Java虚拟机hotspot需要的字节码文件。比如Python的.pyc,就是Python虚拟机cpython需要的字节码文件。字节码转为真正的机器码运行,是虚拟机干的事
机器码长啥样呢
机器码为什么长这样呢?它是按什么规范生成的呢
其实编程语言的优化,大部分都是在机器码层面做文章
比如字节码解释器的执行效率不如模板解释器,原理是什么
原理是编译器会按照固定的框架进行编译生成程序,而模板解释器是一种机器码编制技术,只会生成聚集业务处理的代码。
如图中,如果只是干int a=0,gcc会生成这么多代码,意思就是虚拟机执行这个赋值要执行这么多代码。而模板解释器就是没有打叉的那些,只需要运行那一行有效代码。执行的代码量少这么多,自然效率高很多
当然还有其他的优化机器,你如果真正能看懂这个例子,你自然也能get到,如果你看不懂,那你需要补点机器码的基础了
《手写编程语言》三期,带你使用机器码编织技术重写执行引擎,玩明白了这个,你就能轻松get即时编译及后端优化的各种技术!
至此,速通编译原理就讲完了。如果我文章中的一切你都看懂了,能get到,编译原理你就算过关了
后续还会更新更多超硬核文章,感兴趣的话可以关注**【硬核子牙】**微信公众号