语法:用规则描述一门语言
解析器的任务,是把一串扁平的记号搭成一棵有层次的树。可它凭什么知道该怎么搭 —— 谁是谁的孩子、哪里该套一层?凭一套规则。这套规则叫文法(grammar),它是「一门语言的合法句子长什么样」的精确描述。这一章我们学会读写文法,你会发现它长得像递归 —— 因为语言本来就是递归的。
从「造句」说起
你小时候学过「主语 + 谓语 + 宾语」。这就是一条文法规则 —— 它没告诉你具体说什么,只规定了合法的句子该有什么形状。编程语言的文法是一回事,只是精确得多、没有例外。
写文法有一套通用记法,叫 BNF(巴科斯范式)。一条规则长这样:规则名 → 它由什么组成。先看 Lox 里最简单的一条,「一个基本表达式(primary)是什么」:
primary → NUMBER | STRING | "true" | "false" | "nil" | IDENTIFIER
读作:一个 primary,要么是个数字、要么是个字符串、要么是 true / false / nil、要么是个标识符。竖线 | 是「或者」。这里有两种角色的东西:
- 终结符(terminal):不能再拆的东西,就是记号本身 ——
NUMBER、"true"、IDENTIFIER。它们是树的叶子。(大写的是记号类型,带引号的是具体那个词。) - 非终结符(non-terminal):能继续展开的规则名 —— 比如
primary。它们是树的枝干,展开后指向别的规则或终结符。
递归:语言能无限嵌套,规则也能
关键来了。表达式能无限嵌套:1 + 2 是表达式,(1 + 2) * 3 是表达式,((1 + 2) * 3) - 4 还是表达式…… 一门有限的文法,怎么描述这种无限?
靠递归。让规则引用自己(直接或间接):
expression → NUMBER | "(" expression ")" | expression "+" expression
看到没 —— expression 的定义里又出现了 expression。这一条递归规则,就描述了无穷多个合法表达式:括号里可以再套表达式,加号两边可以再是表达式,套多少层都行。「有限的规则描述无限的语言」,就是靠这一手。
盯住这件事:因为文法是递归的,所以解析它的代码会是递归的,解析出的树也会是递归的(嵌套的)。这条递归的线索,会一路贯穿接下来三章 —— 文法递归引用 → 解析函数递归调用 → 语法树递归嵌套 → 求值器递归遍历。
如果你学过《求值》那本书,这里会有一声回响:那里我们说「代码就是一棵树」,而这里,我们正在写出那棵树的生成规则。同一件事,两个角度。
那个逼出一切的难题:歧义
上面那条 expression → expression "+" expression 有个致命毛病。用它去解析 1 + 2 * 3(假设也有一条 * 的规则),你会发现它能搭出两棵不同的树:
树 A:(1 + 2) * 3 → 先加后乘,结果 9 树 B:1 + (2 * 3) → 先乘后加,结果 7
两棵树,两个结果,都符合这条文法。这就叫歧义(ambiguity)—— 一句话能被理解成不止一种结构。而歧义对编程语言是致命的:1 + 2 * 3 必须有唯一确定的意思,不能看解析器心情。
你当然知道正确答案是树 B(先乘后加,7)—— 因为「乘法优先级比加法高」。但文法本身不知道这件事。那条朴素的递归规则里,压根没写「谁先谁后」。
所以核心问题变成了:怎么把「优先级」和「结合性」这种规矩,编进文法里,让它对每个句子只能搭出唯一一棵树?答案很巧妙,是把文法分层 —— 但那是第 9 章的高潮。这一章你只需要牢牢记住:解析的头号敌人是歧义,而消除歧义是整个文法设计的核心动机。
Lox 表达式文法(分好层的版本)
先把成品亮给你看,混个眼熟 —— 这就是消除了歧义、分好层的 Lox 表达式文法(越往下优先级越高):
expression → equality
equality → comparison ( ( "!=" | "==" ) comparison )*
comparison → term ( ( ">" | ">=" | "<" | "<=" ) term )*
term → factor ( ( "-" | "+" ) factor )*
factor → unary ( ( "/" | "*" ) unary )*
unary → ( "!" | "-" ) unary | primary
primary → NUMBER | STRING | "true" | "false" | "nil"
| "(" expression ")" | IDENTIFIER
现在你未必看得懂它凭什么消除了歧义(那个 ( … )* 是「重复零次或多次」的意思)—— 没关系。先记住它长这样:一叠规则,每一条都调用「比自己更紧一层」的规则。下一章我们把这叠规则逐条翻译成函数,第 9 章再揭穿它消除歧义的魔法。这张表,是接下来两章的乐谱。
回流
BNF 这种「规则名 → 组成」的记法,一旦你认识它,会在文档里到处撞见。
语言规范:ECMAScript 规范、Kotlin 语法参考、Python 官方文档的 Grammar 一节 —— 全是用 BNF(或它的变体 EBNF / PEG)写的。以前你可能跳过那些 Statement → ... 的段落,现在你能读了:它们精确地告诉你「这门语言里什么能写、什么不能」。有疑问「这个语法到底合不合法」,查文法比试错靠谱。
数据格式:JSON 的完整定义就是一小页 BNF;Protobuf 的 .proto、GraphQL 的 schema、SQL 的语法,背后都有文法。你写正则时用的 ( )、|、*,正是从 BNF 借来的元字符 —— 正则本身就是一种描述文法的语言。
下次要给一个自定义格式(配置文件、DSL、协议)定规矩,别用大白话描述,试试写几行 BNF。它逼你把「合法」定义得没有歧义 —— 这个训练本身,就值回这一章。
这一章的一句话
文法是「合法句子长什么样」的递归规则,而它最大的敌人是歧义 —— 让 1 + 2 * 3 只能搭出唯一一棵树,是整个文法设计的核心动机。
下一章,我们把这叠文法规则,机械地翻译成一组互相调用的函数 —— 这就是递归下降,最直观的解析法。