Skip to main content

nichlink/registry_core/syntax/
nesting.rs

1//! The nesting pre-scan that keeps a stack overflow out of `syn`.
2//! 把栈溢出挡在 `syn` 之外的嵌套预扫描。
3//!
4//! `syn` is recursive descent without a depth guard of its own, and
5//! `proc-macro2` guards only its lexer, so a deeply nested source **aborts the
6//! process**: a stack overflow is not a catchable panic, and no `Result` can
7//! report it. `parse_faces(&"(".repeat(60_000))` is the smallest reproducer.
8//! `syn` 是没有自带深度守卫的递归下降解析器,而 `proc-macro2` 只守了它自己的词法器,因此
9//! 深层嵌套的源码会让进程 **abort**:栈溢出不是可捕获的 panic,任何 `Result` 都报不出来。
10//! `parse_faces(&"(".repeat(60_000))` 是最小的复现。
11//!
12//! Three shapes are measured here, and the third is the one that took a second
13//! pass to find: delimiter groups (`(`, `[`, `{`), generic-argument chains
14//! (`Vec<Vec<…>>`), and a **linear run** — tokens the parser folds into one
15//! nested expression or type without any delimiter or angle bracket to count,
16//! such as `& & & …`, `* * * …`, `! ! ! …`, `|| || || …` or `1 + 1 + 1 + …`.
17//!
18//! The third shape is why this is about the *tree* and not only the parse: a run
19//! of binary operators is parsed by a loop, but it produces a left-nested
20//! `ExprBinary` whose **`Drop` recurses once per operator**, so 60 000 `+` tokens
21//! abort the process the same way 60 000 parentheses do. Measured: a run of
22//! 16 384 survives even a 256 KiB stack, while 60 000 aborts on an eight-megabyte
23//! one, which is why the run limit below is a thousand tokens rather than the
24//! parser-recursion limit of 128.
25//!
26//! The scan runs on `proc-macro2`'s own token stream rather than on the raw text,
27//! because that is where string literals and comments have already been removed —
28//! a lexical scan of the text would count the brackets inside a `"…"` and refuse a
29//! source rustc accepts. The walk is iterative for the same reason the guard
30//! exists: measuring nesting must not itself nest.
31//! 这里量三种形状,而第三种是第二轮才找到的:定界符组(`(`、`[`、`{`)、泛型实参链
32//! (`Vec<Vec<…>>`),以及**线性串**——解析器会折叠成一个嵌套表达式或类型、却没有任何定界符
33//! 或尖括号可供计数的 token 串,例如 `& & & …`、`* * * …`、`! ! ! …`、`|| || || …` 或
34//! `1 + 1 + 1 + …`。
35//!
36//! 第三种形状正是这件事关乎**树**而不只是解析的原因:一串二元运算符是用循环解析的,但它产出
37//! 一个左嵌套的 `ExprBinary`,而它的 **`Drop` 每个运算符递归一层**,因此六万个 `+` token 与
38//! 六万个括号一样会让进程 abort。实测:16 384 个的串即使在 256 KiB 栈上也活着,而 60 000 个
39//! 在八兆栈上会 abort——这就是下面的串上限取一千个 token、而不是解析递归上限 128 的原因。
40//!
41//! 扫描跑在 `proc-macro2` 自己的 token 流上而不是原始文本上,因为字符串字面量与注释在那里已经
42//! 被剔除——按文本做词法扫描会把 `"…"` 里的括号算进去,从而拒绝一份 rustc 能接受的源码。遍历是
43//! 迭代的,理由与守卫本身相同:量嵌套这件事自己不能嵌套。
44
45use std::fmt;
46
47use super::face::{FaceSyntaxError, syntax_error};
48
49/// The deepest nesting a source may reach.
50/// 源码允许达到的最大嵌套深度。
51///
52/// 128 is `rustc`'s own default `recursion_limit`, so nothing a compiler accepts
53/// is refused here, and the value is a number someone else already chose for the
54/// same job.
55/// 128 是 `rustc` 自己的默认 `recursion_limit`,因此编译器能接受的源码不会在这里被拒,
56/// 而这个数字是别人为同一件事已经选过的。
57pub(crate) const LIMIT: usize = 128;
58
59/// The longest linear run a source may reach, in tokens.
60/// 源码允许达到的最长线性串,以 token 计。
61///
62/// A run is measured between separators: `,` and `;` end one, and so does a brace
63/// group, because a body or an item ends a chain in real code. Parentheses and
64/// brackets do not end one, since `x.f().f()…` and `foo(a, b)` both keep folding
65/// across them.
66///
67/// 1024 is chosen from measurements rather than taste: a run of 16 384 survives
68/// even a 256 KiB stack while 60 000 aborts an eight-megabyte one, so this is a
69/// sixteen-fold margin below the smallest run ever observed to survive, and this
70/// repository's own longest run is about 91 tokens. The gate that keeps it honest
71/// is `core/tests/nesting_budget.rs`: it feeds every Rust file in the workspace to
72/// the guarded entry point and fails if the guard refuses one. The parser-recursion
73/// limit of [`LIMIT`] does not apply here: these tokens produce one nested tree
74/// rather than nested parser calls.
75/// 1024 取自实测而不是口味:16 384 个的串即使在 256 KiB 栈上也活着,而 60 000 个能在八兆栈上
76/// abort,因此这里比"观察到能活下来的串里最小的那个"还低十六倍,而本仓库自己最长的一条串约
77/// 91 个 token。让它保持诚实的是 `core/tests/nesting_budget.rs`:它把工作区里每个 Rust 文件都
78/// 喂给带守卫的入口,只要守卫拒了其中一个就失败。[`LIMIT`] 那条解析递归上限在这里不适用:
79/// 这些 token 产出的是**一棵**嵌套树,而不是嵌套的解析调用。
80pub(crate) const CHAIN_LIMIT: usize = 1024;
81
82/// Which nesting shape went past [`LIMIT`].
83/// 哪一种嵌套形状越过了 [`LIMIT`]。
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub(crate) enum Shape {
86    /// `(`, `[` and `{` groups.
87    /// `(`、`[` 与 `{` 组。
88    Delimiters,
89    /// A run of tokens folded into one nested expression or type, with no
90    /// delimiter to count: `& & & …`, `1 + 1 + …`, `Vec<Vec<…>>`.
91    /// 被折叠成一个嵌套表达式或类型、却没有定界符可数的 token 串:`& & & …`、
92    /// `1 + 1 + …`、`Vec<Vec<…>>`。
93    ///
94    /// A generic-argument chain used to have its own counter here
95    /// ([`Shape::Arguments`]) that could never fire: it reset on every identifier and
96    /// every generic argument begins with one, so the deepest `Vec<Vec<u8>>` it ever
97    /// saw was one. The variant was deleted, and the chain shape above was supposed to
98    /// cover the cases — but a comma resets a chain, and `X<u8, X<u8, …>>` has a comma
99    /// at *every* level, so the chain saw a run of one while `syn` descended hundreds
100    /// of levels. The counter is therefore back, measuring angle brackets directly
101    /// instead of identifiers. See [`Shape::Arguments`].
102    /// 泛型实参链过去在这里有自己的计数器([`Shape::Arguments`]),却永远不会触发:它每遇到一个
103    /// 标识符就清零,而每个泛型实参都以标识符开头,因此它见过的最深 `Vec<Vec<u8>>` 只有一层。
104    /// 该变体被删除,并指望上面的串形状覆盖这些情况——但逗号会重置一条串,而
105    /// `X<u8, X<u8, …>>` **每一层**都有逗号,于是链看到的串长度是 1,而 `syn` 下潜了几百层。
106    /// 因此这个计数器回来了,直接度量尖括号而不是标识符。见 [`Shape::Arguments`]。
107    Chain,
108    /// Angle-bracket nesting: `<` opens, `>` closes.
109    /// 尖括号嵌套:`<` 打开,`>` 闭合。
110    ///
111    /// Counted on the brackets themselves rather than on the identifiers between them,
112    /// which is the only way a comma-separated generic chain is measurable at all.
113    /// 数的是括号本身,而不是它们之间的标识符——这是带逗号的泛型链唯一可度量的方式。
114    Arguments,
115}
116
117impl fmt::Display for Shape {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        formatter.write_str(match self {
120            Self::Delimiters => "delimiters",
121            Self::Chain => "tokens folded into one expression",
122            Self::Arguments => "generic arguments",
123        })
124    }
125}
126
127/// A measured nesting depth above [`LIMIT`].
128/// 实测超过 [`LIMIT`] 的嵌套深度。
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub(crate) struct TooDeep {
131    /// Which shape was too deep.
132    /// 过深的是哪种形状。
133    pub(crate) shape: Shape,
134    /// How deep it actually went.
135    /// 实际深到多少层。
136    pub(crate) depth: usize,
137    /// The limit that applies to `shape`, so the message names the number the
138    /// reader has to compare against instead of one borrowed from another shape.
139    /// 适用于 `shape` 的上限,使消息说明读者真正要比对的那个数字,而不是从另一种形状借来的。
140    pub(crate) limit: usize,
141}
142
143impl fmt::Display for TooDeep {
144    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145        write!(
146            formatter,
147            "input nests {} levels of {}, above the limit of {}; \
148             it is refused because the parser would overflow the stack instead of reporting an error",
149            self.depth, self.shape, self.limit
150        )
151    }
152}
153
154/// Refuse a source whose nesting would overflow the parser.
155/// 拒绝一份嵌套会让解析器栈溢出的源码。
156///
157/// A source the lexer already refuses is left to `syn`: the scan exists for input
158/// that lexes fine and then recurses, and taking over the lexer's own diagnostic
159/// would only lose the position it carries.
160/// 词法器已经拒绝的源码留给 `syn` 报:本扫描是为"能词法通过、随后递归"的输入存在的,
161/// 抢过词法器自己的诊断只会丢掉它携带的位置。
162pub(crate) fn guard(source: &str) -> Result<(), TooDeep> {
163    let Ok(stream) = source.parse::<proc_macro2::TokenStream>() else {
164        return Ok(());
165    };
166    let mut stack: Vec<(proc_macro2::TokenTree, usize)> =
167        stream.into_iter().map(|tree| (tree, 0)).collect();
168    // Consecutive `Ident`/`<`/`>` tokens are the only run that can grow a generic
169    // argument chain; every other token kind ends the run. That keeps a file full
170    // of `a < b` comparisons from ever reaching the limit, while `Vec<Vec<…>>`
171    // grows one level per `<`.
172    // 连续出现的 `Ident`/`<`/`>` 是唯一能长出泛型实参链的序列,任何其他词法单元都会终结该
173    // 序列。因此通篇 `a < b` 比较的文件永远到不了上限,而 `Vec<Vec<…>>` 每遇一个 `<` 长一层。
174    // Tokens folded into one tree since the last separator. `chain += 1` appears
175    // in every arm that continues a run, so a new token kind cannot silently
176    // escape the measurement the way the prefix operators escaped the first
177    // version of this scan.
178    // 自上一个分隔符以来被折叠进同一棵树的 token 数。每个延续串的分支里都写着 `chain += 1`,
179    // 因此新增一种词法单元不会像前缀运算符逃过本扫描的第一版那样,静默地逃过这个度量。
180    let mut chain = 0usize;
181    // Angle brackets nest the parser too, and a comma does not end that nesting:
182    // `X<u8, X<u8, …>>` has a comma at every level, so the chain counter below — which
183    // a comma resets, because a comma really does end a linear run — saw a run of one
184    // while `syn` descended hundreds of levels and overflowed the stack. This counter
185    // measures that shape directly. The walk pops from a stack, so it visits a group's
186    // tokens right to left; for balanced nesting that means every closer is seen before
187    // its opener, and the maximum reached is the true depth. Unbalanced input that still
188    // lexes either trips this counter or fails in `syn` on its own terms.
189    // 尖括号同样让解析器下潜,而逗号并不结束这种嵌套:`X<u8, X<u8, …>>` 每一层都有逗号,因此
190    // 下面的链计数(逗号会重置它,因为逗号确实结束一条线性串)看到的串长度是 1,而 `syn` 下潜了
191    // 几百层并撑爆栈。这个计数器直接度量那种形状。遍历从栈里 pop,因此它按从右到左访问一个组的
192    // token;对配平的嵌套而言,每个闭合都在其打开之前出现,于是达到的最大值就是真实深度。能词法
193    // 通过却不配平的输入,要么触发这个计数器,要么在 `syn` 那里按它自己的规则失败。
194    let mut angle = 0usize;
195    while let Some((tree, depth)) = stack.pop() {
196        let mut grows = true;
197        match tree {
198            proc_macro2::TokenTree::Group(group) => {
199                let depth = depth + 1;
200                if depth > LIMIT {
201                    return Err(TooDeep {
202                        shape: Shape::Delimiters,
203                        depth,
204                        limit: LIMIT,
205                    });
206                }
207                // A brace group is a body or an item: it ends a chain. The other
208                // delimiters stay inside the expression that holds them, which is
209                // what lets `x.f().f()…` be counted at all.
210                // 花括号组是函数体或条目:它终结一条串。其他定界符留在持有它们的表达式内部,
211                // 这正是 `x.f().f()…` 能被数到的原因。
212                if group.delimiter() == proc_macro2::Delimiter::Brace {
213                    grows = false;
214                }
215                stack.extend(group.stream().into_iter().map(|tree| (tree, depth)));
216            }
217            proc_macro2::TokenTree::Punct(punct) if punct.as_char() == '<' => {
218                angle += 1;
219                if angle > LIMIT {
220                    return Err(TooDeep {
221                        shape: Shape::Arguments,
222                        depth: angle,
223                        limit: LIMIT,
224                    });
225                }
226            }
227            proc_macro2::TokenTree::Punct(punct) if punct.as_char() == '>' => {
228                // `->` and `=>` also carry a `>`; saturating at zero keeps a close
229                // without an open from borrowing depth from the next chain.
230                // `->` 与 `=>` 也带一个 `>`;在零处饱和,使没有对应打开的闭合不会从下一条串借
231                // 深度。
232                angle = angle.saturating_sub(1);
233            }
234            proc_macro2::TokenTree::Punct(punct)
235                if punct.as_char() == ',' || punct.as_char() == ';' =>
236            {
237                grows = false;
238            }
239            _ => {}
240        }
241        if grows {
242            chain += 1;
243            if chain > CHAIN_LIMIT {
244                return Err(TooDeep {
245                    shape: Shape::Chain,
246                    depth: chain,
247                    limit: CHAIN_LIMIT,
248                });
249            }
250        } else {
251            chain = 0;
252        }
253    }
254    Ok(())
255}
256
257/// Refuse a source whose nesting would overflow `syn`, for callers that parse
258/// text themselves.
259/// 对自行解析文本的调用方:拒绝一份嵌套会让 `syn` 栈溢出的源码。
260///
261/// `parse_file` applies this to every `syn::parse_file` in this crate. It is
262/// public because it is the *only* nesting measurement in the workspace: the
263/// documentation gate in `conventions` hands fenced Rust to `syn` too, and a
264/// pathological fence would abort that gate's process rather than fail it. A
265/// second copy of the heuristic there would be a second thing to keep honest;
266/// asking the kernel keeps one.
267/// `parse_file` 把本函数施加于本 crate 里每一处 `syn::parse_file`。它公开是因为它是本工作区
268/// **唯一**的嵌套度量:`conventions` 里的文档门禁也会把围栏 Rust 交给 `syn`,而一份病态围栏
269/// 会让那道门禁的进程 abort 而不是失败。在那里复制一份启发式就等于多一个需要保持诚实的东西;
270/// 向内核发问则只有一份。
271pub fn guard_nesting(source: &str) -> Result<(), FaceSyntaxError> {
272    guard(source).map_err(|error| FaceSyntaxError {
273        message: error.to_string(),
274        location: None,
275    })
276}
277
278/// Parse one Rust file, refusing nesting that would overflow the parser.
279/// 解析一个 Rust 文件,并拒绝会让解析器栈溢出的嵌套。
280///
281/// Every `syn::parse_file` call site in this crate goes through here, so the
282/// guard cannot be forgotten at a new entry point without the code looking
283/// wrong.
284/// 本 crate 里每一处 `syn::parse_file` 都经这里,因此新增入口若漏掉守卫,代码看上去就是错的。
285pub(crate) fn parse_file(source: &str) -> Result<syn::File, FaceSyntaxError> {
286    guard(source).map_err(|error| FaceSyntaxError {
287        message: error.to_string(),
288        location: None,
289    })?;
290    syn::parse_file(source).map_err(|error| syntax_error(error.span(), error.to_string()))
291}