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.pyconfigure.py:513)驱动,结果已签入仓库(详见 代码结构分析 §4.2)。读逻辑看 .in.cc.cc 只是把正则展开成 DFA 状态机(yybm 查找表)。

1.2 Token 类型

enum Tokenlexer.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,其余 varnameIDENT。读完调 EatWhitespace()(除非是 NEWLINE/TEOF)。
  • PeekTokenlexer.in.cc:168)——读一个 token,匹配则消费、否则 UnreadToken() 回退。这是解析器处理可选 |/||/|@/INDENT 的前瞻原语。UnreadToken 只是 ofs_ = last_token_lexer.in.cc:119)。
  • ReadEvalStringlexer.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::Errorlexer.in.cc:28)从输入起点扫到 last_token_\n 来重算行列号,格式化成 filename:line: message,再附一行上下文和 ^ near here 的指示符。DescribeLastErrorlexer.in.cc:109)会特判行首 Tab → “tabs are not allowed, use spaces”——Ninja 用缩进表示变量块,且只接受空格。

2. Parser 基类

struct Parserparser.h:26)是 ManifestParserDyndepParser 的共同基类,持 State*FileReader* 和一个 Lexer

  • Parser::Loadparser.cc:22)——把整个文件读进内存(ReadFile不是 mmap),再调虚函数 Parse。注意它用 METRIC_RECORD_IF(".ninja parse", parent == NULL),让嵌套 include 不重复计时(parser.cc:26)。
  • Parser::ExpectTokenparser.cc:40)——读一个 token,不符就报 “expected X, got Y” + 提示。

3. ManifestParser:语句 → State

ManifestParser::Parsemanifest_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

ParseRulemanifest_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

ParseEdgemanifest_parser.cc:216)是 manifest 解析的核心,顺序处理一条 build 语句:

build out.o | out.d : cc in.c | header.h || order_dir |@ validate.stamp
#     ^^^^^   ^^^^^   ^^ ^^^^   ^^^^^^^^    ^^^^^^^^^    ^^^^^^^^^^^^^^^
#     显式输出 隐式输出 规则 显式输入 隐式输入   order-only   validations
  1. 显式输出:循环 ReadPathouts_:221
  2. 隐式输出:PeekToken(PIPE) 后继续读,计 implicit_outs:234
  3. ExpectToken(COLON),读规则名 → env_->LookupRule:254,沿作用域链查,找不到报 “unknown build rule”)
  4. 显式输入 → 隐式输入(PIPE)→ order-only(PIPE2)→ validations(PIPEAT),分别累计计数(:262-310
  5. 处理缩进的边级变量绑定:若有,分配子作用域 BindingEnv(env_),每个值立即在父作用域求值再绑到边的 env 上(:315
  6. state_->AddEdge(rule) 建边并设 edge->env_:328
  7. 解析 pool 绑定 → state_ 查池(:331
  8. 输出/输入/validations 逐个 EvaluateCanonicalizePathstate_->AddOut/AddIn/AddValidation,并写回三段计数 implicit_outs/implicit_deps/order_only:339-382

一个值得注意的边界:若一条边的输出经去重后全部已存在(被别的边声明过),这条边会被丢弃(edges_.pop_back(); delete edge;:352)。

3.3 include 与 subninja 的作用域差异

ParseFileIncludemanifest_parser.cc:423):

  • include:子解析器共享当前 env_,被包含文件的定义直接进入当前作用域。
  • subninja:子解析器拿一个新建的子作用域 new BindingEnv(env_),其定义不会泄漏到上层。

两者都调 subparser_->Load(path, err, &lexer_),把当前 lexer 作为 parent 传入,用于错误链路和计时抑制。

4. 变量求值与作用域

4.1 EvalString:切好的 token 列表

EvalStringeval_env.h:35)是一串 (string, TokenType)TokenTypeRAW(字面)或 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:作用域链

BindingEnveval_env.h:93)含 bindings_(变量)、rules_(规则)和 parent_ 指针。

  • LookupVariableeval_env.cc:21)——本层找不到就递归 parent_,到顶返回 ""。这是全局/subninja 的作用域上溯。
  • LookupWithFallbackeval_env.cc:95)——实现边变量查找的三级优先级(文档见 eval_env.h:107):
    1. 边自己设的值;
    2. 否则规则上设的值,在边的作用域里展开
    3. 否则上层作用域的值。

4.3 EdgeEnv:构建期真正的求值引擎

边求值命令时用的是 EdgeEnvgraph.cc:405),它是独立于 BindingEnv 的一个 Env

  • $in/$in_newline/$out 直接从节点向量按三段计数切片合成(graph.cc:423,见 构建图模型 §2.2)。
  • 其他变量:取规则的 EvalStringrule_->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. 路径规范化

每个解析出的路径都过 CanonicalizePathutil.cc:124)。性能关键版本是就地改写的 CanonicalizePath(char*, size_t*, uint64_t*)util.cc:141):

  • 丢空组件(foo//barfoo/bar)、丢 ../foofoo)、回退解析 ..util.cc:215-236);
  • 非 Windows 用 memchr 加速扫描(util.cc:188);
  • 空结果变成 "."aa/...)。

slash_bitsutil.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

下一篇 构建执行流程 把这张构建好的图交给 PlanBuilder 去执行。


本站访问量

This site uses Just the Docs, a documentation theme for Jekyll.