Manifest 解析:Lexer、Parser 与变量求值
本文讲 build.ninja 文本如何变成内存里的 构建图。整条流水线是:
.ninja 字节流
→ Lexer (re2c) 切词 + 收集 EvalString 片段 lexer.{h,in.cc,cc}
→ Parser 基类 读文件 / 期待 token parser.{h,cc}
→ ManifestParser 语句 → State 对象 manifest_parser.{h,cc}
→ EvalString/BindingEnv 变量求值与作用域 eval_env.{h,cc} + graph.cc(EdgeEnv)
→ CanonicalizePath 规范化每个路径 util.cc
1. Lexer:词法分析
1.1 re2c 生成
词法器逻辑写在 lexer.in.cc(含 /*!re2c ... */ 块),由 re2c 生成 lexer.cc(首行 /* Generated by re2c */)。生成由 configure.py(configure.py:513)驱动,结果已签入仓库(详见 代码结构分析 §4.2)。读逻辑看 .in.cc,.cc 只是把正则展开成 DFA 状态机(yybm 查找表)。
1.2 Token 类型
enum Token(lexer.h:32):
enum Token {
ERROR, BUILD, COLON, DEFAULT, EQUALS, IDENT, INCLUDE,
INDENT, NEWLINE, PIPE, PIPE2, PIPEAT, POOL, RULE, SUBNINJA, TEOF,
};
其中 PIPE=|(隐式依赖/输出)、PIPE2=||(order-only)、PIPEAT=|@(validations)、INDENT=缩进(变量块)。
1.3 核心扫描方法
ReadToken(逻辑 lexer.in.cc:123)——读一个 token。re2c 规则块里定义了varname = [a-zA-Z0-9_.-]+,关键字(build/rule/pool/…)和运算符映射到对应 token,其余varname归IDENT。读完调EatWhitespace()(除非是NEWLINE/TEOF)。PeekToken(lexer.in.cc:168)——读一个 token,匹配则消费、否则UnreadToken()回退。这是解析器处理可选|/||/|@/INDENT的前瞻原语。UnreadToken只是ofs_ = last_token_(lexer.in.cc:119)。ReadEvalString(lexer.in.cc:213)——核心:把路径或变量值扫成一个EvalString。它通过ReadPath(path 模式)和ReadVarValue(value 模式)两个 inline 包装调用(lexer.h:80)。
1.4 $ 转义与变量引用
ReadEvalString 处理所有 $ 转义,边扫边调 eval->AddText / eval->AddSpecial:
| 输入 | 含义 | 行 |
|---|---|---|
$$ | 字面 $ | lexer.in.cc:240 |
$ | 字面空格 | lexer.in.cc:244 |
$: | 字面冒号 | lexer.in.cc:262 |
$\n + 空格 | 续行(吞掉) | lexer.in.cc:248 |
${varname} | 变量引用 | lexer.in.cc:254 |
$simple_varname | 变量引用 | lexer.in.cc:258 |
$^ | 字面换行(需 ninja_required_version >= 1.14) | lexer.in.cc:266 |
$. 其他 | 报错 “bad $-escape” | lexer.in.cc:280 |
path 模式下,空格/:/|/换行会终止当前 token;value 模式下只有换行终止,:/|/空格都是字面文本。这正是为什么 build 语句里路径不能含未转义空格,而规则的 command = 值可以随便写。
1.5 错误定位
Lexer::Error(lexer.in.cc:28)从输入起点扫到 last_token_ 数 \n 来重算行列号,格式化成 filename:line: message,再附一行上下文和 ^ near here 的指示符。DescribeLastError(lexer.in.cc:109)会特判行首 Tab → “tabs are not allowed, use spaces”——Ninja 用缩进表示变量块,且只接受空格。
2. Parser 基类
struct Parser(parser.h:26)是 ManifestParser 和 DyndepParser 的共同基类,持 State*、FileReader* 和一个 Lexer。
Parser::Load(parser.cc:22)——把整个文件读进内存(ReadFile,不是 mmap),再调虚函数Parse。注意它用METRIC_RECORD_IF(".ninja parse", parent == NULL),让嵌套include不重复计时(parser.cc:26)。Parser::ExpectToken(parser.cc:40)——读一个 token,不符就报 “expected X, got Y” + 提示。
3. ManifestParser:语句 → State
ManifestParser::Parse(manifest_parser.cc:39)是顶层分发循环,按首 token 派发到各处理函数:
POOL → ParsePool (manifest_parser.cc:100)
RULE → ParseRule (manifest_parser.cc:140)
BUILD → ParseEdge (manifest_parser.cc:216)
DEFAULT → ParseDefault (manifest_parser.cc:191)
IDENT → 顶层 name=value 绑定(特判 ninja_required_version)
INCLUDE/SUBNINJA → ParseFileInclude
为减少内存分配,ManifestParser 跨边复用 ins_/outs_/validations_ 缓冲和一个 subparser_(manifest_parser.h:73)——又一处”速度优先”。
3.1 ParseRule
ParseRule(manifest_parser.cc:140)读规则名,拒绝当前作用域内重名(LookupRuleCurrentScope),然后对每个缩进的 key = value:key 必须通过 Rule::IsReservedBinding 校验,否则报 “unexpected variable”。值以未求值的 EvalString 存进 rule->AddBinding(key, value)——规则变量是延迟到每条边才展开的。最后校验 command 必须存在、rspfile/rspfile_content 必须同时有或同时无。
3.2 ParseEdge:build 语句 → Edge
ParseEdge(manifest_parser.cc:216)是 manifest 解析的核心,顺序处理一条 build 语句:
build out.o | out.d : cc in.c | header.h || order_dir |@ validate.stamp
# ^^^^^ ^^^^^ ^^ ^^^^ ^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^
# 显式输出 隐式输出 规则 显式输入 隐式输入 order-only validations
- 显式输出:循环
ReadPath进outs_(:221) - 隐式输出:
PeekToken(PIPE)后继续读,计implicit_outs(:234) ExpectToken(COLON),读规则名 →env_->LookupRule(:254,沿作用域链查,找不到报 “unknown build rule”)- 显式输入 → 隐式输入(
PIPE)→ order-only(PIPE2)→ validations(PIPEAT),分别累计计数(:262-310) - 处理缩进的边级变量绑定:若有,分配子作用域
BindingEnv(env_),每个值立即在父作用域求值再绑到边的 env 上(:315) state_->AddEdge(rule)建边并设edge->env_(:328)- 解析
pool绑定 →state_查池(:331) - 输出/输入/validations 逐个
Evaluate→CanonicalizePath→state_->AddOut/AddIn/AddValidation,并写回三段计数implicit_outs/implicit_deps/order_only(:339-382)
一个值得注意的边界:若一条边的输出经去重后全部已存在(被别的边声明过),这条边会被丢弃(edges_.pop_back(); delete edge;,:352)。
3.3 include 与 subninja 的作用域差异
ParseFileInclude(manifest_parser.cc:423):
include:子解析器共享当前env_,被包含文件的定义直接进入当前作用域。subninja:子解析器拿一个新建的子作用域new BindingEnv(env_),其定义不会泄漏到上层。
两者都调 subparser_->Load(path, err, &lexer_),把当前 lexer 作为 parent 传入,用于错误链路和计时抑制。
4. 变量求值与作用域
4.1 EvalString:切好的 token 列表
EvalString(eval_env.h:35)是一串 (string, TokenType),TokenType 为 RAW(字面)或 SPECIAL(变量名)。一个优化:若只有单个 RAW token,存进 single_token_ 而不用 vector,省一次分配(eval_env.h:58)。Evaluate(Env*)(eval_env.cc:111)遍历这串 token,RAW 原样拼接、SPECIAL 调 env->LookupVariable(name)。
4.2 BindingEnv:作用域链
BindingEnv(eval_env.h:93)含 bindings_(变量)、rules_(规则)和 parent_ 指针。
LookupVariable(eval_env.cc:21)——本层找不到就递归parent_,到顶返回""。这是全局/subninja 的作用域上溯。LookupWithFallback(eval_env.cc:95)——实现边变量查找的三级优先级(文档见 eval_env.h:107):- 边自己设的值;
- 否则规则上设的值,在边的作用域里展开;
- 否则上层作用域的值。
4.3 EdgeEnv:构建期真正的求值引擎
边求值命令时用的是 EdgeEnv(graph.cc:405),它是独立于 BindingEnv 的一个 Env:
$in/$in_newline/$out直接从节点向量按三段计数切片合成(graph.cc:423,见 构建图模型 §2.2)。- 其他变量:取规则的
EvalString(rule_->GetBinding(var)),再env_->LookupWithFallback(var, eval, this)——传this(EdgeEnv)作为展开作用域,正是这一步让$in/$out在规则变量展开时可见。 - 环检测:
lookups_是当前正在展开的变量名栈,重复出现就Fatal("cycle in rule variables: ...")(graph.cc:469)。
综合起来,一个名字在边上的有效优先级是:
$in/$out/$in_newline (实时计算)
> 边级绑定
> 规则绑定 (在边作用域展开)
> 上层/全局作用域
这套机制就是为什么你能在规则里写 command = $cc $cflags -c $in -o $out,而 $cflags 既可以在规则里给默认值、又能被某条边覆盖、还能引用全局变量。
5. 路径规范化
每个解析出的路径都过 CanonicalizePath(util.cc:124)。性能关键版本是就地改写的 CanonicalizePath(char*, size_t*, uint64_t*)(util.cc:141):
- 丢空组件(
foo//bar→foo/bar)、丢.(./foo→foo)、回退解析..(util.cc:215-236); - 非 Windows 用
memchr加速扫描(util.cc:188); - 空结果变成
"."(aa/..→.)。
slash_bits(util.cc:291)只在 Windows 有意义:它记录规范化后每个 / 原本是不是 \,把反斜杠改成 / 的同时在位掩码里置位,以便显示/执行命令时反规范化回原样(PathDecanonicalized,用于 EdgeEnv::MakePathList)。非 Windows 下恒为 0。
6. 小结:行号速查
| 主题 | 位置 |
|---|---|
| Token 枚举 | lexer.h:32 |
ReadEvalString($ 转义) | lexer.in.cc:213 |
| 错误定位 | lexer.in.cc:28 |
Parser::Load | parser.cc:22 |
ManifestParser::Parse | manifest_parser.cc:39 |
ParseEdge | manifest_parser.cc:216 |
| include/subninja 作用域 | manifest_parser.cc:423 |
EvalString | eval_env.h:35 |
BindingEnv::LookupWithFallback | eval_env.cc:95 |
EdgeEnv($in/$out、环检测) | graph.cc:405 |
CanonicalizePath / slash_bits | util.cc:124 / util.cc:291 |
下一篇 构建执行流程 把这张构建好的图交给 Plan 和 Builder 去执行。