从零编写现代编译器 04 - 表达式解析与错误恢复
第 03 篇给了编译器完整的词法分析器和源码位置。解析器已经能识别函数定义和 return 语句,但 return 后面那个表达式还只能接受单个整数字面量。return 1 + 2 * 3; 在当前解析器面前会直接报错——它不知道运算符是什么,更不知道 * 应该比 + 先结合。这一篇要解决这个问题:用 Pratt 解析实现一个完整的表达式解析器,正确处理优先级和结合性,并在遇到语法错误时恢复到可继续解析的状态。 优先级表 表达式里运算符的优先级决定了 AST 的形状。1 + 2 * 3 应该解析为 1 + (2 * 3) 而不是 (1 + 2) * 3。Sprout 语言的优先级从低到高: 优先级 运算符 结合性 说明 1 || 左结合 逻辑或,短路 2 && 左结合 逻辑与,短路 3 == != 左结合 相等比较 4 < > <= >= 左结合 大小比较 5 + - 左结合 加减 6 * / 左结合 乘除 7 一元 - ! 前缀 取负、逻辑非 所有二元运算符都是左结合。a + b + c 解析...
从零编写现代编译器 03 - 语句解析:let、赋值和分号
上一篇把源码拆成了 Token 序列。一行 let mut x: i64 = 0; 经过词法分析后变成 Let、Mut、Ident("x")、Colon、Ident("i64")、Assign、Int(0)、Semi 这样一串扁平记号。但 Token 序列不包含结构信息——编译器还不知道这是一条变量声明、一条赋值还是一条返回语句。这一篇的任务是把 Token 流解析成结构化的语句节点:let 声明、赋值、return 和块。 语句的文法 先用一组产生式把 Sprout 当前支持的语句写下来。这里只处理语句层面,表达式的优先级和结合性留给第 04 篇。 12345678910111213stmt → let_stmt | assign_stmt | return_stmt | expr_stmtlet_stmt → "let" ["mut"] IDENT ":" type "=" exp...
从零编写现代编译器 02 - 词法分析器:把文本切成 Token
上一篇里,编译器能处理的输入是硬编码的——它只认识 fn main() -> i64 { return 42; } 这一个程序,Token 序列直接写死在代码里。换一个数字就得改编译器源码。这不叫编译,这叫字符串替换。 这一篇要做的事情:给 Sprout 写一个词法分析器(lexer),让它读入任意源文本,逐字符扫描,输出一串带位置信息的 Token。后续的解析器将消费这个 Token 流,而不再关心原始字符。 词法分析器做什么 编译器前端的第一道工序是词法分析。它的输入是源文件的字节流,输出是 Token 序列。每个 Token 标记了一段源文本的语法角色:这是一个关键字,那是一个整数字面量,这里是左括号,那里是分号。 词法分析器不关心 Token 之间的组合是否合法。return return return 在词法层面完全没有问题——三个 Return Token。判断组合是否合法是解析器的工作。词法分析器只负责切割。 类比一下:词法分析像把一段中文拆成词语,每个词标上词性。"我吃了三碗饭"变成 [我/代词] [吃/动词] [了/助词] [三/数词...
从零编写现代编译器 01 - return 42:从源码到可执行文件
一个只返回 42 的函数,从源码文本变成本机可执行文件,中间到底经历了什么?这篇把整条路走一遍:手动分词、构造语法树、生成 LLVM IR、编译成目标文件、链接为二进制、运行并检查退出码。走完这条最短路径之后,后续每篇只需往流水线里加东西。 最小的程序 Sprout 语言的第一个合法程序只有一行有效逻辑: 123fn main() -> i64 { return 42;} fn 声明函数,main 是入口,-> i64 标注返回类型为 64 位有符号整数,函数体只做一件事——返回整数字面量 42。这个程序没有变量、没有条件、没有循环,但它足以驱动编译器的每一个阶段。 编译器要做的事情可以用一条流水线概括: 1源码 → 词法分析 → 语法分析 → 代码生成 → LLVM 工具链 → 可执行文件 下面逐段走完这条线。 源码到 Token:词法分析 词法分析器(lexer)的工作是把字符流切成有意义的最小单元——Token。对上面那段源码,手动分词的结果是: 1234567891011FN -- 关键字 fnIDENT(...
从零编写现代编译器 00 - 这门语言准备编译什么
多数编译器教程止步于计算器——解析 1 + 2 * 3,求值,结束。这个系列的终点不同:从一个空目录开始,逐步构建一个能检查类型错误、生成本机可执行文件、报告源码位置并接入编辑器的编译器。 目标语言暂名 Sprout,扩展名 .spr,编译器命令暂定 sproutc。实现语言是 Rust,主后端是 LLVM。进阶部分还会接入 Cranelift 即时编译(Just-In-Time compilation, JIT)、WebAssembly 组件和 MLIR 张量编译。 这篇导读划定语言范围、列出编译架构、给出语义边界草案并确认先修条件。代码从第 01 篇开始;本篇没有可运行的编译器。 本篇目标 读完这篇,需要能回答一个问题:给定一段 Sprout 源码 fn main() -> i64 { return 42; },区分源语言、实现语言、中间表示和机器码四个层次。 这个区分贯穿整个系列。源语言是读者设计的 Sprout,实现语言是 Rust,中间表示包括 AST、HIR、SSA 和 LLVM IR,机器码是目标架构的二进制指令。混淆任何两层都会在后续调试中制造困惑。 最终...
现代编译器写作计划:从 Rust 到 LLVM、Wasm 和 MLIR
这不是一篇“把一个表达式计算器写出来”的短教程,而是一套可以拆成连载的编译器写作计划。目标有三个:读者能跟着写,例子能跟着跑,技术选型不会很快过时。技术锚点按 2026-09-05 的官方文档来定:Rust 1.98.1 / Rust 2024 edition,LLVM 23.1.0,Wasmtime 45.0.0,WASI 0.2/0.3 preview,以及仍在活跃演进中的 MLIR。 这本书要写什么 这套教程的主线不是“如何写一个最小可运行编译器”,而是“如何写一条今天仍然成立的编译器路线”。前半部分讲前端:词法、语法、名字绑定、类型检查、错误恢复。中间部分讲 IR:AST、typed HIR、SSA、控制流图、数据流分析。后半部分讲后端:LLVM IR、Cranelift、WebAssembly component model、WASI,再加一章 MLIR 作为多层 IR 的扩展方向。 这条路线的好处很直接。读者先学到经典编译器骨架,再看到现代工具链怎么把这条骨架接到真实系统上。写完以后,代码可以落到原生二进制,也可以落到 Wasm component;同一套中端还能顺着...
分布式 ID 系统设计:从需求约束推导架构
数据库自增列足以支撑单库业务。系统拆成多个写入节点后,ID 生成突然变成一项基础设施能力:它要避免碰撞,不能拖慢写路径,还要经受扩容、时钟异常、数据库切换、配置漂移和跨地域网络分区。 这类系统最容易从算法名称开始讨论:UUID、Snowflake、数据库序列、号段。算法当然重要,但架构并不是从算法列表里挑出来的。它来自一组可验证的约束:唯一到什么范围,顺序要强到什么程度,故障时允许停多久,ID 可以暴露什么信息,以及团队能维护多少协调状态。 先定义 ID 的合同 “生成一个全局唯一且递增的 ID”看似明确,实际混合了几种不同语义。若不拆开,评审阶段达成的共识很可能只是每个人对同一句话的不同理解。 唯一性有作用域 唯一性至少要说明三个边界: 命名空间:全公司、单业务、单租户、单表,还是单个分片。 时间范围:进程存活期间、数据保留期,还是系统整个生命周期。 保证方式:确定性不重叠,还是碰撞概率低到工程上可接受。 数据库序列和合理分配的 Snowflake 节点空间可以给出确定性的不重叠保证。UUIDv4 依靠足够大的随机空间降低碰撞概率。两者都常被称为“全局唯一”,但证明路径并...
tmux 教程:把一次 SSH 登录变成可恢复的终端工作台
终端窗口关闭、SSH 连接抖动或笔记本合盖,常常会把正在运行的构建、日志观察和远程编辑一起打断。tmux 的价值不在于把屏幕切成几块,而在于把程序运行的归属从一次 terminal client 连接中分离出来:client 可以离开,session 仍留在主机上。 tmux 是 terminal multiplexer,不是终端模拟器,也不是进程监督器。它运行在已有终端里,管理一组带 pseudo-terminal 的程序;它能抵抗 terminal 或网络连接中断,却不会让程序跨主机重启自动恢复。这个边界决定了 tmux 适合管理交互式工作台,不应被当成 systemd、容器编排器或备份系统的替代品。 先建立正确的对象关系 tmux server 管理 client、session、window 和 pane。client attach 到一个 session;session 维护 window 列表;window 由一个或多个 pane 组成;每个 pane 都承载一个 pseudo-terminal。window 还可以链接给多个 session,因此这不是一棵严格的 ...
深入 HBase 19 - HBase 3.0 与设计边界
HBase 3.0.0 是大版本升级,不应被写成 2.6 的小补丁。官方下载页显示 3.0.0 发布于 2026-08-05,同时 2.6.6 发布于 2026-06-09,2.5.15 被标为 stable release。基础机制仍可用 2.6.6 解释,但 3.0 的部署、客户端连接和 API 兼容边界必须单独处理。 本篇只抓一个核心问题:3.0 相对 2.6 改了什么;哪些工作负载应该选别的数据库。 版本位置图 flowchart TD A[HBase 2.6.6 基础机制] --> B[row key / Region / WAL / MemStore / HFile] B --> C[2.x 客户端与运维习惯] C --> D[HBase 3.0.0 大版本升级] D --> E[JDK 17+] D --> F[Hadoop 2 移除] D --> G[MasterRegistry/RPC bootstrap 默认路径] D --> H[deprecated/private API 清理] H...
深入 HBase 18 - Metrics、hbtop、Compaction 与性能调优
HBase 性能问题很少只由一个参数造成。热点 Region、StoreFile 过多、compaction backlog、BlockCache 命中率下降、GC 停顿、HDFS locality 变差,都会表现成读写延迟上升。调优的第一步不是改配置,而是把症状映射到可观察对象。 本篇只抓一个核心问题:如何用 Metrics、hbtop、JMX 和服务端状态识别热点 Region、compaction backlog、缓存失配、GC 和磁盘瓶颈。 观察路径图 flowchart TD Slow[读写变慢] --> Client[Client retry/timeout] Slow --> RS[RegionServer metrics] RS --> Hot[Region #REQ/S/#WRITE/S] RS --> Cache[BlockCache hit/miss] RS --> Store[#SF / StoreFile size] RS --> Compaction[Compaction queue/prog...
