# 编译原理入门 ::: tip 前言 **当你按下"运行"按钮,代码是怎么变成屏幕上的结果的?** 你写的每一行代码,计算机其实都"看不懂"——它只认识 0 和 1。编译器就是那个把人类语言翻译成机器语言的"翻译官"。理解编译原理,你就能理解报错信息从哪来、为什么有些语言快有些慢、以及代码优化的底层逻辑。 ::: **这篇文章会带你学什么?** 学完这章后,你将获得: - **全局视野**:掌握从源代码到可执行程序的完整编译流水线 - **词法分析**:理解编译器如何把代码拆成一个个 Token - **语法分析**:理解 AST(抽象语法树)的构建过程 - **AST 可视化**:直观看到代码的树形结构 - **语义分析与优化**:理解类型检查和代码优化的原理 - **优化技术实战**:掌握常量折叠、死代码消除等核心优化手段 - **执行模型**:区分编译型、解释型和 JIT 三种执行方式 | 章节 | 内容 | 核心概念 | |-----|------|---------| | **第 1 章** | 编译器是什么 | 翻译官类比、编译流水线 | | **第 2 章** | 词法分析 | Token、词法规则 | | **第 3 章** | 语法分析 | AST、语法树、优先级 | | **第 4 章** | AST 可视化 | 交互式语法树、节点类型 | | **第 5 章** | 语义分析与优化 | 类型检查、常量折叠、死代码消除 | | **第 6 章** | 优化技术实战 | 函数内联、循环外提、常量传播 | | **第 7 章** | 编译型 vs 解释型 vs JIT | 三种执行模型对比 | --- ## 0. 全景图:代码的"翻译之旅" 想象你是一个翻译官,要把一本中文小说翻译成英文。你不会一个字一个字地直译,而是: 1. **识别词语** — 把句子拆成一个个词(词法分析) 2. **理解句法** — 判断句子结构是否正确(语法分析) 3. **理解语义** — 确保意思通顺、没有矛盾(语义分析) 4. **润色优化** — 让译文更地道流畅(代码优化) 5. **输出译文** — 写出最终的英文版本(代码生成) 编译器做的事情完全一样,只不过它翻译的是编程语言。 --- ## 1. 编译器的六步流水线 编译器的工作可以分为六个阶段,像工厂流水线一样,每个阶段处理完交给下一个阶段。 ::: tip 编译流水线 1. **词法分析(Lexical Analysis)**:把源代码拆成一个个 Token(单词) 2. **语法分析(Syntax Analysis)**:把 Token 组织成语法树(AST) 3. **语义分析(Semantic Analysis)**:检查类型是否正确、变量是否声明 4. **中间代码生成(IR Generation)**:生成与平台无关的中间表示 5. **代码优化(Optimization)**:让中间代码更高效 6. **代码生成(Code Generation)**:生成目标平台的机器码 ::: | 阶段 | 输入 | 输出 | 类比 | |------|------|------|------| | 词法分析 | 源代码字符流 | Token 流 | 把句子拆成单词 | | 语法分析 | Token 流 | AST(语法树) | 分析句子结构 | | 语义分析 | AST | 带类型的 AST | 检查意思是否通顺 | | 中间代码 | 带类型的 AST | IR | 写出初稿 | | 代码优化 | IR | 优化后的 IR | 润色删减 | | 代码生成 | 优化后的 IR | 机器码 | 输出终稿 | --- ## 2. 词法分析:把代码拆成"单词" 词法分析是编译的第一步。编译器从左到右扫描源代码的每个字符,把它们组合成有意义的**Token(词法单元)**。 就像读英文句子时,你的大脑会自动把字母组合成单词一样,词法分析器把字符组合成 Token: ``` 源代码: let x = 10 + 5; Token 流: [let] → 关键字(语言保留字) [x] → 标识符(变量名) [=] → 运算符(赋值) [10] → 数字字面量 [+] → 运算符(加法) [5] → 数字字面量 [;] → 分隔符(语句结束) ``` ::: tip Token 的五大类型 - **关键字**:语言保留的特殊单词,如 `let`、`if`、`return`、`function` - **标识符**:程序员定义的名字,如变量名、函数名 - **字面量**:直接写在代码里的值,如数字 `42`、字符串 `"hello"` - **运算符**:执行运算的符号,如 `+`、`-`、`=`、`===` - **分隔符**:分隔代码结构的符号,如 `;`、`,`、`(`、`)` ::: --- ## 3. 语法分析:构建语法树(AST) 词法分析把代码拆成了 Token,但 Token 只是一个个孤立的"单词"。语法分析的任务是把这些 Token 按照语法规则组织成一棵**抽象语法树(Abstract Syntax Tree, AST)**——它反映了代码的结构和运算优先级。 ``` 表达式: 1 + 2 * 3 语法树: 为什么这样? + 因为 * 的优先级 / \ 高于 +,所以 1 * 2 * 3 先结合 / \ 成为一个子树 2 3 ``` ::: tip AST 的重要性 AST 是编译器的"核心数据结构",后续的语义分析、优化、代码生成都基于它进行。现代开发工具也大量使用 AST: - **ESLint**:解析代码为 AST,检查是否违反规则 - **Prettier**:解析为 AST 后重新格式化输出 - **Babel**:解析 AST → 转换 → 生成兼容代码 - **IDE 重构**:基于 AST 进行安全的变量重命名、函数提取 ::: | 语法结构 | Token 序列 | AST 节点 | |---------|-----------|---------| | 变量声明 | `let` `x` `=` `10` | VariableDeclaration → Identifier + Literal | | 函数调用 | `add` `(` `1` `,` `2` `)` | CallExpression → Identifier + Arguments | | 条件语句 | `if` `(` `a` `>` `b` `)` | IfStatement → BinaryExpression + Block | --- ## 4. AST 可视化:看见代码的"骨架" 上面我们用文字描述了 AST 的结构,但"看到"比"读到"更直观。下面的交互组件让你选择不同的表达式,实时观察它们的语法树长什么样。 通过可视化你会发现,AST 的核心规律其实很简单: | 代码结构 | AST 根节点 | 子节点 | |---------|-----------|-------| | `1 + 2 * 3` | BinaryExpression (+) | 左: NumericLiteral(1),右: BinaryExpression(*) | | `let x = 10` | VariableDeclaration | VariableDeclarator → Identifier(x) + NumericLiteral(10) | | `add(a, b)` | CallExpression | Identifier(add) + Arguments(a, b) | ::: tip AST 在日常开发中的应用 你可能没直接写过编译器,但你每天都在用基于 AST 的工具: - **ESLint / Prettier**:解析代码为 AST,检查规则或重新格式化 - **Babel / SWC**:解析 AST → 转换语法 → 生成兼容代码 - **IDE 重构**:基于 AST 做安全的重命名、提取函数 - **Tree-shaking**:分析 AST 中的 import/export,删除未使用的代码 ::: --- ## 5. 语义分析与代码优化 语法分析确保代码"结构正确",但结构正确不代表"意思正确"。语义分析负责检查代码的含义是否合法,代码优化则让程序跑得更快。 ### 4.1 语义分析:检查"意思"对不对 | 检查内容 | 示例 | 结果 | |---------|------|------| | 类型检查 | `int x = "hello"` | ❌ 类型不匹配 | | 作用域检查 | 使用未声明的变量 `y` | ❌ 变量不存在 | | 类型推断 | `1 + 2.0` | ✅ 推断结果为 float | | 参数检查 | `add(1, 2, 3)` 但函数只接受 2 个参数 | ❌ 参数数量不匹配 | ::: tip 你见过的报错,大多来自语义分析 - `TypeError: Cannot read properties of undefined` — 类型检查 - `ReferenceError: x is not defined` — 作用域检查 - `Expected 2 arguments, but got 3` — 参数检查 ::: ### 4.2 代码优化:让程序更快 编译器在生成最终代码前,会对中间代码做各种优化。这些优化对程序员透明,但能显著提升性能。 | 优化技术 | 优化前 | 优化后 | 原理 | |---------|-------|-------|------| | 常量折叠 | `x = 10 + 5` | `x = 15` | 编译时直接算出结果 | | 死代码消除 | `if (false) { ... }` | 直接删除 | 永远不会执行的代码 | | 常量传播 | `x = 15; y = x * 2` | `y = 30` | 已知值直接替换 | | 循环不变量外提 | 循环内重复计算 `len = arr.length` | 提到循环外 | 避免重复计算 | --- ## 6. 优化技术实战:编译器如何让代码更快 上面我们提到了几种优化技术的名字,现在来深入看看编译器具体是怎么做的。下面的交互组件展示了 5 种最常见的编译器优化,你可以直观对比优化前后的代码差异。 现代编译器和 JIT 引擎(如 V8、GCC、LLVM)会自动应用数十种优化。作为开发者,你不需要手动做这些优化,但理解它们能帮你: - **写出更容易被优化的代码**:比如用 `const` 而不是 `let`,编译器更容易做常量折叠 - **理解性能差异**:为什么小函数比大函数快?因为编译器能内联它们 - **避免"反优化"**:某些写法会阻止编译器优化,比如 `eval()` 和 `with` | 优化技术 | 触发条件 | 性能影响 | 开发者能做什么 | |---------|---------|---------|-------------| | 常量折叠 | 表达式中全是常量 | 消除运行时计算 | 多用 const 声明 | | 死代码消除 | 代码不可达或结果未使用 | 减小代码体积 | 及时清理无用代码 | | 循环不变量外提 | 循环内有不变的计算 | 减少重复计算 | 手动提取也是好习惯 | | 函数内联 | 小函数被频繁调用 | 消除调用开销 | 保持函数小而专注 | | 常量传播 | 变量值在编译时可确定 | 整条计算链被消除 | 用常量代替魔法数字 | --- ## 7. 编译型 vs 解释型 vs JIT 代码写完后,有三种"翻译方式"让它运行起来。这三种方式各有优劣,直接决定了语言的性能特征和使用场景。 | 维度 | 编译型 | 解释型 | JIT 即时编译 | |------|-------|-------|------------| | 过程 | 先全量编译成机器码,再执行 | 边读边执行,逐行翻译 | 先解释执行,热点代码再编译 | | 运行速度 | 最快 | 最慢 | 中等(热点接近编译型) | | 启动速度 | 慢(需要编译) | 快(直接运行) | 中等(需要预热) | | 跨平台 | 需要重新编译 | 天然跨平台 | 跨平台 | | 代表语言 | C, Rust, Go | Python, Ruby | JavaScript (V8), Java | ::: tip 为什么 JavaScript 这么快? V8 引擎的 JIT 编译器会监测哪些代码被频繁执行(热点代码),然后把它们编译成高度优化的机器码。所以虽然 JavaScript 是"解释型语言",但在 V8 中它的性能可以接近编译型语言。这也是 Node.js 能做服务端的底气。 ::: --- ## 总结 编译原理不是只有编译器开发者才需要了解的知识。理解编译流程,能帮你更好地理解报错信息、选择合适的语言、写出更高效的代码。 回顾本章的关键要点: 1. **编译器是翻译官**:把人类可读的代码翻译成机器可执行的指令 2. **六步流水线**:词法分析 → 语法分析 → 语义分析 → 中间代码 → 优化 → 代码生成 3. **词法分析拆 Token**:把字符流拆成关键字、标识符、运算符等有意义的单元 4. **语法分析建 AST**:按语法规则把 Token 组织成树形结构,反映运算优先级 5. **语义分析保正确**:类型检查、作用域检查,你见过的大多数报错都来自这里 6. **编译器自动优化**:常量折叠、死代码消除、函数内联等技术让代码自动变快 7. **三种执行模型**:编译型最快、解释型最灵活、JIT 兼顾两者 ## 延伸阅读 - [AST Explorer](https://astexplorer.net/) - 在线查看代码的 AST 结构 - [Crafting Interpreters](https://craftinginterpreters.com/) - 从零实现一门编程语言(免费在线书) - [The Super Tiny Compiler](https://github.com/jamiebuilds/the-super-tiny-compiler) - 用 JavaScript 实现的超小编译器 - [V8 Blog](https://v8.dev/blog) - V8 引擎的 JIT 编译技术博客 - [LLVM 官网](https://llvm.org/) - 最流行的编译器基础设施