文法是用一组产生式规则定义字符串集合的简洁工具。把文法交给解析器生成器(parser generator),就能自动得到一棵具体语法树;再把它翻译成抽象语法树,程序就可以直接处理结构化的数据。
在 6.102 中,文法(grammar)是一种形式化记法,用来描述某个语言中所有合法字符串的集合。它由以下要素组成:
文法:一组产生式规则,定义了哪些字符串属于该语言。每个产生式把一个非终结符映射到终结符与非终结符的一个序列。
为了让文法写得更紧凑,我们使用几种常用运算符:
*:匹配零次或多次。|:匹配左边或右边。?(零次或一次)、+(一次或多次)、{n}(恰好 n 次)、[...](字符类)、[^...](否定字符类)。例如,一个简化算术表达式文法可以写成:
expr = number ('+' number)*
number = digit+
digit = [0-9]
把文法应用到具体字符串上,就得到一棵解析树(parse tree)。树的内部节点是非终结符,叶子节点是终结符,整个结构展示了字符串如何由文法推导出来。
解析树:表示一个字符串如何由某条文法生成的树形结构,记录了每一步产生式的应用。
正则表达式是正则文法的紧凑记法。它擅长描述词法层面的模式,比如标识符、数字、邮箱地址。但正则表达式不能处理任意嵌套的结构(比如匹配括号的表达式),因为正则文法没有「栈记忆」。
// 正则表达式示例:匹配简化邮箱
const email = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
console.log(email.test("ada@mit.edu")); // true
用完整的文法(而不仅是正则表达式)可以表达的语言,称为上下文无关语言(context-free languages)。它们由上下文无关文法(CFG)定义,能处理递归嵌套——这正是编程语言语法的基础。
解析器生成器(parser generator):读入一文法,自动生成一个解析器——一个把字符串转换为解析树(或 AST)的程序。
6.102 使用 ParserLib。它接受一个文法字符串,编译出一个可复用的解析器对象。
import { Parser } from 'parserlib';
const grammar = `
expr = number ('+' number)* ;
number = digit+ ;
digit = [0-9] ;
`;
const parser = new Parser(grammar);
const tree = parser.parse("12+34+5");
解析器输出的是具体语法树(parse tree),它忠实地反映文法,但包含很多与求值无关的细节(括号、分隔符)。我们通常把它翻译成更精简的抽象语法树(AST)。
// 把解析树翻译成 AST
interface Expr { evaluate(): number; }
class Num implements Expr { constructor(public n: number){} evaluate(){return this.n;} }
class Add implements Expr {
constructor(public left: Expr, public right: Expr){}
evaluate(){ return this.left.evaluate() + this.right.evaluate(); }
}
当字符串不符合文法时,ParserLib 会抛出 ParseError。健壮的程序应该捕获它并向用户给出清晰的错误位置和原因。
ParserLib 生成的是自顶向下递归下降(top-down recursive descent)解析器。如果文法中存在左递归(产生式右侧以自身开头),解析器会无限循环。例如:
// 左递归(ParserLib 不支持)
expr = expr '+' number ; // ← 无限循环!
// 改写为右递归或循环形式
expr = number ('+' number)* ;
因此使用 ParserLib 时,必须把左递归改写为等价的右递归或重复形式。