“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
编译器与语言设计入门:从零构建你的编程语言
编译器设计核心概念:词法分析、语法分析、语义分析与代码生成的全流程解析。
编译器与语言设计入门:从零构建你的编程语言
引言:为什么学习编译器设计?
编译器是计算机科学中最经典的“系统软件”之一。它不仅是C、Python、Java等高级语言的幕后推手,更是理解计算机如何执行代码的绝佳窗口。很多程序员对编译器的印象停留在“黑盒”阶段——输入源代码,输出可执行文件,中间发生了什么?其实,编译器设计的核心思想并不神秘,它由几个清晰的阶段组成,每个阶段都有成熟的理论和工具支撑。
本文基于Douglas Thain的《Introduction to Compilers and Language Design》一书的核心内容,系统性地拆解编译器的构建过程。无论你是想自己设计一门领域特定语言(DSL),还是想深入理解编译原理,这篇文章都会给你一个扎实的起点。
编译器的主要阶段
一个典型的编译器可以划分为以下五个阶段:
- 词法分析(Lexical Analysis):将源代码字符串拆分成有意义的“单词”,即词法单元(Token)。
- 语法分析(Parsing):根据语言的语法规则,将Token序列组织成抽象语法树(AST)。
- 语义分析(Semantic Analysis):检查AST是否符合语言的语义规则(如类型检查、作用域解析)。
- 中间代码生成(Intermediate Code Generation):将AST转换为平台无关的中间表示(IR)。
- 代码生成(Code Generation):将IR转换为目标机器码或字节码。
此外,现代编译器通常还包含优化阶段,穿插在中间代码生成和最终代码生成之间,用于提升生成代码的效率。
阶段一:词法分析——把字符串变成Token
词法分析器(Lexer)的任务是读取源文件的字符流,并按照预定义的规则(通常用正则表达式描述)将其切分成Token序列。每个Token通常包含两个信息:类型(如关键字、标识符、数字、运算符)和值(如变量名"x"、数值42)。
例如,对于表达式 a = 42 + b;,词法分析器会输出类似以下的Token序列:
IDENTIFIER("a") ASSIGN INTEGER(42) PLUS IDENTIFIER("b") SEMICOLON
关键实现细节
- 正则表达式:每个Token类型对应一个正则模式,词法分析器使用有限自动机(DFA,确定性有限自动机)来高效匹配。例如,标识符的模式可能是
[a-zA-Z_][a-zA-Z0-9_]*,整数字面量的模式是[0-9]+。 - 最长匹配原则:当多个模式可能匹配时(如
if既是关键字,也是标识符),词法分析器通常选择最长的匹配。如果长度相同,则按模式定义的优先级选择(关键字优先于标识符)。 - 跳过空白与注释:空格、换行、注释通常不产生Token,而是被词法分析器直接忽略。
工具支持
手动编写词法分析器是可行的,但更常见的做法是使用自动生成工具,如Lex(或Flex)和Ragel。你只需提供一组正则表达式和对应的动作代码,工具就会生成高效的C、C++或Rust词法分析器。
阶段二:语法分析——构建语法树
语法分析器(Parser)将Token序列作为输入,根据上下文无关文法(Context-Free Grammar, CFG)构建出抽象语法树(AST)。AST是源代码的树形表示,去除了括号、分号等语法细节,只保留结构关系。
上下文无关文法简介
CFG由一组产生式(Production)组成,每个产生式描述如何将非终结符替换为终结符或非终结符的组合。例如,一个简单算术表达式的文法可以写成:
expr -> expr '+' term | term
term -> term '*' factor | factor
factor -> '(' expr ')' | NUMBER
这里,expr、term、factor是非终结符,+、*、(、)、NUMBER是终结符。
解析方法
- 自顶向下解析(如递归下降解析器):从起始符号(如
expr)开始,尝试匹配输入Token序列。每个非终结符对应一个函数,函数内部根据当前Token选择对应的产生式。递归下降解析器易于手动编写,但要求文法不能有左递归(如expr -> expr '+' term会导致无限递归)。 - 自底向上解析(如LR解析器):从输入Token开始,逐步归约为非终结符,最终归约为起始符号。LR解析器能处理更广泛的文法(包括左递归),但通常需要借助Yacc/Bison等工具自动生成。
抽象语法树(AST)设计
AST的节点类型通常与文法中的非终结符对应。例如,对于加法表达式expr '+' expr,AST可能包含一个AddNode,其左子节点和右子节点分别是两个子表达式。AST不包含括号、分号等语法糖,只保留运算的层次关系。
阶段三:语义分析——为AST赋予意义
语法只检查代码的“形式”是否正确,语义分析则检查代码的“含义”是否合法。主要工作包括:
- 类型检查:确保操作数类型与运算符兼容。例如,在大多数静态类型语言中,
"hello" + 42是不合法的。 - 作用域解析:确认每个变量引用都指向一个已声明的变量,且类型匹配。
- 控制流检查:例如,检查
break语句是否出现在循环内,函数是否在所有路径上都有return语句。
符号表
语义分析的核心数据结构是符号表,它记录了每个作用域中声明的变量、函数、类型等信息。符号表通常以栈的形式实现:进入一个新作用域时压栈,退出时弹栈。每个符号条目包含名称、类型、作用域深度、存储位置等属性。
类型系统
类型系统可以是静态的(编译时检查)或动态的(运行时检查)。在编译器中,类型检查通常基于类型推导或类型标注。例如,在ML或Haskell中,编译器可以自动推导出表达式的类型,而在C或Java中,程序员需要显式声明类型。
阶段四:中间代码生成——平台无关的桥梁
中间表示(IR)是编译器中至关重要的一层。它比AST更接近机器码(通常是三地址码或静态单赋值形式SSA),但又与具体CPU架构无关。使用IR的好处是:
- 可以在IR层面进行跨平台的优化(如常量折叠、死代码消除)。
- 后端代码生成器只需将IR翻译成目标机器码,无需重复实现词法、语法分析。
三地址码(Three-Address Code, TAC)
三地址码的每条指令最多包含三个操作数,例如:
t1 = 42
t2 = b
t3 = t1 + t2
a = t3
这种形式非常适合后续的优化和寄存器分配。
静态单赋值形式(SSA)
SSA是更高阶的IR,要求每个变量只被赋值一次。通过引入“Phi函数”(φ函数)来处理控制流汇合点的变量版本,SSA使得数据流分析更加简单高效。现代编译器(如LLVM、GCC)都使用SSA作为主要IR。
阶段五:代码生成——从IR到机器码
代码生成器将IR转换为目标CPU的汇编指令或机器码。这一阶段涉及多个关键子问题:
- 指令选择:为每条IR指令选择合适的机器指令序列。例如,
t1 = t2 + t3可能对应ADD R1, R2, R3(假设寄存器分配已完成)。 - 寄存器分配:将IR中的虚拟寄存器映射到物理寄存器。由于物理寄存器数量有限(如x86有16个通用寄存器),需要利用图着色算法或线性扫描算法来优化分配。
- 指令调度:重新排列指令顺序以利用CPU的流水线特性,减少停顿(stall)。
寄存器分配:图着色算法
寄存器分配可以建模为图的着色问题:每个虚拟变量是图的一个节点,如果两个变量在程序中的某个点同时活跃,则它们之间有一条边。用K种颜色(对应K个物理寄存器)给图上色,相邻节点不能同色。如果K种颜色不够,就需要将某些变量“溢出”(spill)到内存中。
优化:让代码跑得更快
优化可以在编译器的多个阶段进行,包括:
- 常量折叠(Constant Folding):在编译时计算常量表达式,例如
3 + 5直接替换为8。 - 死代码消除(Dead Code Elimination):删除永远不会执行的代码,例如
if (false) { ... }。 - 循环优化:如循环展开(loop unrolling)、循环不变代码外提(loop-invariant code motion)。
- 内联展开(Inlining):将小函数的调用替换为函数体本身,减少调用开销。
实战:用Flex和Bison构建一个微型编译器
书中提供了一个完整的微型语言示例,称为“C-”(C minus),它包含整数、布尔、数组、函数和基本控制流。构建过程如下:
- 用Flex定义词法规则(标识符、关键字、运算符等)。
- 用Bison定义语法规则(表达式、语句、函数定义等),并生成解析器。
- 在解析过程中构建AST(Bison的动作代码中创建节点)。
- 遍历AST进行语义分析(类型检查、符号表维护)。
- 生成三地址码(或直接翻译成简单的虚拟机指令)。
- 最终输出汇编代码或字节码。
示例:C-语言代码片段
c
int gcd(int a, int b) {
while (a != b) {
if (a > b) {
a = a - b;
} else {
b = b - a;
}
}
return a;
}
经过编译器处理后,会生成对应的三地址码序列,然后翻译成x86汇编。
拓展阅读与工具
- 经典教材:《编译原理》(龙书)、《现代编译原理》(虎书)、《工程编译原理》(鲸书)。
- 现代编译器框架:LLVM(提供完整的IR、优化和代码生成基础设施)、GCC。
- 在线资源:Douglas Thain的《Introduction to Compilers and Language Design》全书免费在线阅读(https://dthain.github.io/books/compiler/)。
结语
编译器设计并非高不可攀的技术。通过理解词法分析、语法分析、语义分析、中间代码生成和代码生成这五个核心阶段,你可以从零开始构建一门简单的编程语言。更重要的是,这种理解会让你对高级语言的运行机制有更深的洞察——无论是调试一个奇怪的bug,还是优化性能瓶颈,编译器知识都能派上用场。