Skip to main content

big_code_analysis_ast/
ast.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::enum_glob_use, clippy::if_not_else, clippy::wildcard_imports)]
8
9//! The AST dump: [`AstNode`], [`Span`], [`AstCfg`], [`AstResponse`], and
10//! the walk that builds them.
11
12use serde::{Deserialize, Serialize};
13
14use crate::*;
15
16/// Start and end positions of a node in a code in terms of lines, columns,
17/// and byte offsets.
18///
19/// Serialized as a flat object
20/// `{start_line, start_col, end_line, end_col, start_byte, end_byte}`. The
21/// line/column pairs are 1-based; the byte offsets are 0-based half-open
22/// (`[start_byte, end_byte)`) indices into the parsed source bytes
23/// (`big_code_analysis::Ast::source`). The `*_line` vocabulary aligns the
24/// `/ast` span field names with the `/function` and `/metrics` endpoints
25/// (`start_line` / `end_line`), so a client correlating spans across
26/// endpoints no longer special-cases `*_row` vs `*_line` per endpoint
27/// (#638). The former `start_row` / `end_row` keys were renamed as a
28/// `2.0`-line break.
29///
30/// The byte offsets let structural consumers slice the original source for a
31/// node — including internal nodes, whose `value` text the dump omits — so a
32/// caller can recover any subtree's exact bytes without re-deriving offsets
33/// from lines and columns (#727). They mirror tree-sitter's own
34/// `Node::start_byte` / `Node::end_byte`.
35///
36/// The struct is `#[non_exhaustive]`: construct it through [`Span::new`] and
37/// read its public fields, but do not rely on struct-literal construction or
38/// exhaustive destructuring from outside the crate. This is the last planned
39/// shape break to the type. The two byte fields carry `#[serde(default)]` so
40/// span objects serialized before they existed still deserialize (the
41/// offsets default to `0`).
42///
43/// A node's span is `None` for the root and any node when span tracking is
44/// disabled; in that case the wrapping `Option<Span>` serializes as `null`.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
46#[non_exhaustive]
47pub struct Span {
48    /// Line of the start position (1-based).
49    pub start_line: usize,
50    /// Column of the start position (1-based).
51    pub start_col: usize,
52    /// Line of the end position (1-based).
53    pub end_line: usize,
54    /// Column of the end position (1-based).
55    pub end_col: usize,
56    /// Byte offset of the node's first byte in the source (0-based).
57    #[serde(default)]
58    pub start_byte: usize,
59    /// Byte offset one past the node's last byte in the source (0-based,
60    /// exclusive).
61    #[serde(default)]
62    pub end_byte: usize,
63}
64
65impl Span {
66    /// Builds a [`Span`] from 1-based line/column pairs and 0-based,
67    /// half-open byte offsets (`[start_byte, end_byte)`).
68    ///
69    /// This is the supported construction path now that the struct is
70    /// `#[non_exhaustive]`.
71    #[must_use]
72    pub fn new(
73        start_line: usize,
74        start_col: usize,
75        end_line: usize,
76        end_col: usize,
77        start_byte: usize,
78        end_byte: usize,
79    ) -> Self {
80        Self {
81            start_line,
82            start_col,
83            end_line,
84            end_col,
85            start_byte,
86            end_byte,
87        }
88    }
89}
90
91/// The payload of an `Ast` request.
92///
93/// Unknown fields are rejected with a deserialization error naming the
94/// offending key, so a typo'd field cannot silently change request
95/// semantics (#633). The web boundary renders that as a `400` carrying the
96/// `unknown_field` `error_kind` token.
97#[derive(Debug, Deserialize, Serialize)]
98#[serde(deny_unknown_fields)]
99pub struct AstPayload {
100    /// The id associated to a request for an `AST`.
101    ///
102    /// Optional on the wire (#645): an omitted `id` deserializes to the
103    /// empty string, which every downstream surface already treats as
104    /// "no correlation id". Defaulting it stops clients eating a `400`
105    /// for a field whose absence has an obvious meaning.
106    #[serde(default)]
107    pub id: String,
108    /// The filename associated to a source code file
109    pub file_name: String,
110    /// The code to be represented as an `AST`
111    pub code: String,
112    /// If `true`, nodes representing comments are ignored. Optional on
113    /// the wire (#645): omitting it defaults to `false`, matching the
114    /// `bool` default and the most common request shape.
115    #[serde(default)]
116    pub comment: bool,
117    /// If `true`, the start and end positions of a node in a code
118    /// are considered. Optional on the wire (#645): omitting it defaults
119    /// to `false`.
120    #[serde(default)]
121    pub span: bool,
122}
123
124/// The response of an `AST` request.
125///
126/// The envelope echoes the resolved `language` slug alongside `id` and
127/// `root`, matching the `/function`, `/comment`, and `/metrics` analysis
128/// endpoints (#654). AST node kinds are grammar-specific, so an `/ast`
129/// consumer most needs to confirm which grammar actually parsed the
130/// source. `language` is the #540 canonical lowercase slug (the same value
131/// the sibling endpoints emit). The added field is a `2.0`-line shape
132/// change to this published library type (STABILITY.md).
133#[derive(Debug, Serialize)]
134pub struct AstResponse {
135    /// The id associated to a request for an `AST`
136    pub id: String,
137    /// The resolved source-language slug that produced this tree (#654).
138    ///
139    /// The #540 canonical lowercase slug (e.g. `cpp`, `python`), matching
140    /// the other analysis endpoints' `language` echo.
141    pub language: String,
142    /// The root node of an `AST`
143    ///
144    /// If `None`, an error has occurred
145    pub root: Option<AstNode>,
146}
147
148/// Greatest AST depth [`AstNode`] will serialize.
149///
150/// An `AstNode` tree is as deep as the parsed expression nesting, which a
151/// caller controls directly — 100 000 levels fit in a 200 KB payload, well
152/// inside `bca-web`'s body cap. `serde` cannot emit a tree without one
153/// native stack frame per level, and overflowing that stack aborts the
154/// process rather than raising a catchable panic (#1056), so the depth is
155/// bounded: a deeper tree fails serialization with an ordinary serializer
156/// error naming the limit.
157///
158/// The bound is set well clear of real source: the deepest AST across the
159/// ~8 000-file corpus under `tests/repositories` (TensorFlow, serde,
160/// DeepSpeech, …) is 188 levels. It is also set well clear of the stack:
161/// the earliest measured overflow of any emitted format was 2 000 levels
162/// on a debug build's default 2 MiB thread.
163///
164/// Read that 188 as a corpus figure, not a conversion rate. The depth
165/// counted here is the **`AstNode` tree after [`Alterator::alterate`]**,
166/// and how many levels one level of *source* nesting costs is a
167/// per-language property of what that alterator flattens. The Tcl family
168/// is the outlier: since #1381 a script body keeps its children, so one
169/// brace level spans `braced_word` → `command` → `word_list` and costs
170/// three levels here — 196 levels over 64 nested `eval` braces, measured
171/// by `a_tcl_brace_level_costs_three_ast_levels` below — putting the
172/// ceiling near 170 nested braces rather than 512. Nothing in the corpus
173/// approaches either number — it holds no Tcl at all — but a caller
174/// re-deriving this bound from the 188 alone would be reasoning about
175/// the wrong quantity.
176///
177/// [`Alterator::alterate`]: crate::alterator::Alterator::alterate
178pub const MAX_AST_SERIALIZE_DEPTH: usize = 512;
179
180/// Serializes an [`AstNode`]'s children one level deeper, refusing to
181/// descend past [`MAX_AST_SERIALIZE_DEPTH`].
182fn serialize_children<S: serde::Serializer>(
183    children: &[AstNode],
184    serializer: S,
185) -> Result<S::Ok, S::Error> {
186    crate::recursion::serialize_bounded(children, MAX_AST_SERIALIZE_DEPTH, "AstNode", serializer)
187}
188
189/// Information on an `AST` node.
190///
191/// Serialized as a flat object with `snake_case` keys: `type`, `value`,
192/// `span`, `field_name`, `children`.
193#[derive(Debug, Serialize)]
194#[serde(rename_all = "snake_case")]
195pub struct AstNode {
196    /// The type of node
197    pub r#type: &'static str,
198    /// The code associated to a node
199    pub value: String,
200    /// The start and end positions of a node in a code
201    pub span: Option<Span>,
202    /// Tree-sitter grammar field name through which the parent reaches
203    /// this node (e.g. `left`, `right`, `name`, `body`).
204    ///
205    /// `None` for the root node, anonymous tokens (punctuation, keywords),
206    /// and any child that does not occupy a named grammar field. Consumers
207    /// of the JSON output rely on this to distinguish structurally
208    /// equivalent children without grammar-specific positional knowledge.
209    pub field_name: Option<&'static str>,
210    /// The children of a node
211    #[serde(serialize_with = "serialize_children")]
212    pub children: Vec<AstNode>,
213}
214
215// AST depth is caller-controlled — 100 000 levels of nested parentheses
216// fit in a 200 KB payload — so the compiler-generated `Drop` glue would
217// recurse once per level and abort the process (#1056). See
218// [`crate::recursion`].
219crate::recursion::impl_iterative_drop!(AstNode, children);
220
221impl AstNode {
222    /// Builds an `AstNode` with the supplied type, value, span, and
223    /// children. The `field_name` is set to `None`; use
224    /// [`AstNode::with_field_name`] to record the tree-sitter grammar
225    /// field through which the parent reaches this node.
226    #[must_use]
227    pub fn new(
228        r#type: &'static str,
229        value: String,
230        span: Option<Span>,
231        children: Vec<AstNode>,
232    ) -> Self {
233        Self::with_field_name(r#type, value, span, None, children)
234    }
235
236    /// Builds an `AstNode` carrying the tree-sitter grammar field name
237    /// (`left`, `right`, `name`, `body`, ...) through which the parent
238    /// reaches this node.
239    #[must_use]
240    pub fn with_field_name(
241        r#type: &'static str,
242        value: String,
243        span: Option<Span>,
244        field_name: Option<&'static str>,
245        children: Vec<AstNode>,
246    ) -> Self {
247        Self {
248            r#type,
249            value,
250            span,
251            field_name,
252            children,
253        }
254    }
255}
256
257fn build<T: ParserTrait>(parser: &T, span: bool, comment: bool) -> Option<AstNode> {
258    // Iterative depth-first walk that materializes `AstNode`s bottom-up.
259    // Each frame holds the pending parent node, the grammar field name
260    // through which its own parent reached it (None for the root), the
261    // already-materialized child `AstNode`s, and the next child index to
262    // descend into. The parent's `field_name_for_child(idx)` lookup is
263    // O(1) and avoids the parallel cursor walk that was required when
264    // field names had to be captured via `TreeCursor::field_name()`.
265    struct Frame<'a> {
266        node: crate::Node<'a>,
267        field: Option<&'static str>,
268        children: Vec<AstNode>,
269        next_child_index: usize,
270    }
271
272    let code = parser.code();
273    let root = parser.root();
274    let mut stack: Vec<Frame<'_>> = vec![Frame {
275        node: root,
276        field: None,
277        children: Vec::with_capacity(root.child_count()),
278        next_child_index: 0,
279    }];
280    // Mirrors `stack`'s nodes, so popping both leaves `chain` holding
281    // exactly the popped node's ancestry. `Alterator::keeps_children`
282    // is the only consumer, and it is the reason this exists rather
283    // than climbing with `Node::parent`: that lookup is `O(depth)` and
284    // asking it per node made `Ast::dump` quadratic in nesting depth —
285    // 10 ms against 418 ms on 8 KB of nested Tcl braces, measured by
286    // swapping this for `Ancestors::unknown()` (#1381, the shape of
287    // #1052 / #1122).
288    //
289    // That swap is invisible to the test suite: an unknown chain
290    // answers identically and only costs more, so nothing here pins the
291    // chain by asserting on output. What pins it is `Ancestors::checked`
292    // below — its `debug_assert!` catches a desynchronised truncate/push
293    // on every fixture the dump walk sees, and `make chain-audit`
294    // upgrades that to the exact `chain.last() == node.parent()` check.
295    let mut chain: Vec<crate::Node<'_>> = vec![root];
296
297    loop {
298        let frame = stack
299            .last_mut()
300            .expect("stack invariant: loop only runs while stack is non-empty");
301        let child_count = frame.node.child_count();
302        if frame.next_child_index < child_count {
303            let idx = frame.next_child_index;
304            frame.next_child_index += 1;
305            // `Node::child` is O(1) (direct tree-sitter pointer
306            // arithmetic); `field_name_for_child` returns the static
307            // grammar field for that child position. Tree-sitter caps
308            // child indices at u32, so the cast is safe by invariant.
309            let child = frame
310                .node
311                .child(idx)
312                .expect("stack invariant: idx < child_count so the child exists");
313            let field = frame.node.field_name_for_child(
314                u32::try_from(idx).expect("invariant: tree-sitter caps child indices at u32::MAX"),
315            );
316            stack.push(Frame {
317                node: child,
318                field,
319                children: Vec::with_capacity(child.child_count()),
320                next_child_index: 0,
321            });
322            chain.push(child);
323        } else {
324            let frame = stack
325                .pop()
326                .expect("stack invariant: just observed non-empty via last_mut()");
327            chain.pop();
328            let node = T::Checker::get_ast_node(
329                &frame.node,
330                code,
331                span,
332                comment,
333                frame.field,
334                frame.children,
335                Ancestors::checked(&chain, &frame.node),
336            );
337            match (node, stack.last_mut()) {
338                (Some(ast), Some(parent)) => parent.children.push(ast),
339                (Some(ast), None) => return Some(ast),
340                (None, None) => return None,
341                (None, Some(_)) => {}
342            }
343        }
344    }
345}
346
347/// Configuration options for retrieving the nodes of an `AST`.
348#[derive(Debug)]
349pub struct AstCfg {
350    /// The id associated to a request for an `AST`
351    pub id: String,
352    /// The resolved source-language slug to echo in the response
353    /// envelope (#654). The #540 canonical lowercase slug.
354    pub language: String,
355    /// If `true`, nodes representing comments are ignored
356    pub comment: bool,
357    /// If `true`, the start and end positions of a node in a code
358    /// are considered
359    pub span: bool,
360}
361
362/// Build the AST dump for `parser` under `cfg`. Backs `big_code_analysis::Ast::dump`;
363/// the AST-extraction analogue of `big_code_analysis::spaces::metrics_inner` /
364/// `big_code_analysis::ops::ops_inner`.
365pub fn dump_inner<T: ParserTrait>(parser: &T, cfg: AstCfg) -> AstResponse {
366    AstResponse {
367        id: cfg.id,
368        language: cfg.language,
369        root: build(parser, cfg.span, cfg.comment),
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use std::path::PathBuf;
376
377    use super::*;
378
379    #[cfg(any(feature = "cpp", feature = "rust", feature = "tcl"))]
380    fn build_ast<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
381        let path = PathBuf::from(filename);
382        let parser = P::new(code.to_vec(), &path, None);
383        let cfg = AstCfg {
384            id: String::new(),
385            language: String::new(),
386            comment: false,
387            span: false,
388        };
389        dump_inner(&parser, cfg)
390            .root
391            .expect("parser should produce a root AST node")
392    }
393
394    #[cfg(feature = "rust")]
395    fn build_ast_with_span<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
396        let path = PathBuf::from(filename);
397        let parser = P::new(code.to_vec(), &path, None);
398        let cfg = AstCfg {
399            id: String::new(),
400            language: String::new(),
401            comment: false,
402            span: true,
403        };
404        dump_inner(&parser, cfg)
405            .root
406            .expect("parser should produce a root AST node")
407    }
408
409    #[cfg(any(feature = "cpp", feature = "rust"))]
410    fn find_first<'a>(node: &'a AstNode, kind: &str) -> Option<&'a AstNode> {
411        if node.r#type == kind {
412            return Some(node);
413        }
414        node.children.iter().find_map(|c| find_first(c, kind))
415    }
416
417    #[cfg(any(feature = "cpp", feature = "rust"))]
418    fn find_child<'a>(parent: &'a AstNode, field: &str) -> Option<&'a AstNode> {
419        parent.children.iter().find(|c| c.field_name == Some(field))
420    }
421
422    #[cfg(feature = "rust")]
423    #[test]
424    fn root_has_no_field_name() {
425        let root = build_ast::<crate::RustParser>(b"fn main() {}", "test.rs");
426        assert_eq!(root.field_name, None);
427    }
428
429    #[cfg(feature = "rust")]
430    #[test]
431    fn rust_assignment_carries_left_and_right_field_names() {
432        // `assignment_expression` in the Rust grammar names its operands
433        // `left` and `right`. Without `FieldName` exposed in the JSON,
434        // downstream consumers cannot distinguish the two `identifier`
435        // children. This is the canonical example from issue #244.
436        let root =
437            build_ast::<crate::RustParser>(b"fn f() { let mut a = 0; a = a + 1; }", "test.rs");
438        let assign = find_first(&root, "assignment_expression")
439            .expect("expected an assignment_expression node");
440        let left = find_child(assign, "left").expect("expected a `left` child");
441        let right = find_child(assign, "right").expect("expected a `right` child");
442        assert_eq!(left.field_name, Some("left"));
443        assert_eq!(right.field_name, Some("right"));
444        // Anonymous `=` token is a child too, with no field name.
445        assert!(
446            assign
447                .children
448                .iter()
449                .any(|c| c.r#type == "=" && c.field_name.is_none()),
450            "expected the `=` token child to carry no field name; got {:?}",
451            assign
452                .children
453                .iter()
454                .map(|c| (c.r#type, c.field_name))
455                .collect::<Vec<_>>(),
456        );
457    }
458
459    #[cfg(feature = "rust")]
460    #[test]
461    fn rust_function_carries_name_and_body_field_names() {
462        // `function_item` names children `name`, `parameters`, `body`.
463        // Assert the field name directly on the AstNode so a bug that
464        // misnames a field (e.g. always emits "body") fails even if
465        // the target node kinds coincidentally line up.
466        let root =
467            build_ast::<crate::RustParser>(b"fn greet(name: &str) -> &str { name }", "test.rs");
468        let func = find_first(&root, "function_item").expect("expected a function_item node");
469        let name_child = find_child(func, "name").expect("function_item should have a name child");
470        assert_eq!(name_child.field_name, Some("name"));
471        assert_eq!(name_child.r#type, "identifier");
472        let params_child =
473            find_child(func, "parameters").expect("function_item should have a parameters child");
474        assert_eq!(params_child.field_name, Some("parameters"));
475        assert_eq!(params_child.r#type, "parameters");
476        let body_child = find_child(func, "body").expect("function_item should have a body child");
477        assert_eq!(body_child.field_name, Some("body"));
478        assert_eq!(body_child.r#type, "block");
479    }
480
481    #[cfg(feature = "cpp")]
482    #[test]
483    fn cpp_assignment_carries_left_and_right_field_names() {
484        // Cross-language confirmation: the C/C++ grammar uses the same
485        // `left`/`right` field names for `assignment_expression`.
486        let root =
487            build_ast::<crate::CppParser>(b"int main(){ int x = 0; x = x + 1; }", "test.cpp");
488        let assign = find_first(&root, "assignment_expression")
489            .expect("expected an assignment_expression node");
490        assert_eq!(
491            find_child(assign, "left").map(|n| n.r#type),
492            Some("identifier")
493        );
494        assert_eq!(
495            find_child(assign, "right").map(|n| n.r#type),
496            Some("binary_expression")
497        );
498    }
499
500    #[cfg(feature = "rust")]
501    #[test]
502    fn serialized_json_includes_field_name_key() {
503        // Regression for the Serialize derive: every node must serialize
504        // a `field_name` key (null or string). Verifying via JSON
505        // string-match catches accidental removal of the field from
506        // the serializer.
507        let root = build_ast::<crate::RustParser>(b"fn f(){ let a = 1; }", "test.rs");
508        let json = serde_json::to_string(&root).expect("serialize");
509        assert!(
510            json.contains("\"field_name\""),
511            "field_name missing from JSON: {json}"
512        );
513        // The let binding's `pattern` and `value` fields should both
514        // appear as string values in the JSON.
515        assert!(
516            json.contains("\"field_name\":\"pattern\""),
517            "expected pattern field name; got {json}"
518        );
519        assert!(
520            json.contains("\"field_name\":\"value\""),
521            "expected value field name; got {json}"
522        );
523    }
524
525    #[cfg(feature = "rust")]
526    #[test]
527    fn serialized_json_uses_snake_case_keys() {
528        // The serialized AST shape uses snake_case keys (#535). This
529        // anchors the key scheme against accidental reversion to the
530        // former PascalCase `Type`/`TextValue`/`Span`/`Children`.
531        let root = build_ast_with_span::<crate::RustParser>(b"fn f(){}", "test.rs");
532        let json = serde_json::to_string(&root).expect("serialize");
533        for key in [
534            "\"type\":",
535            "\"value\":",
536            "\"span\":",
537            "\"field_name\":",
538            "\"children\":",
539        ] {
540            assert!(json.contains(key), "expected key {key}; got {json}");
541        }
542        for legacy in ["\"Type\"", "\"TextValue\"", "\"Span\"", "\"Children\""] {
543            assert!(
544                !json.contains(legacy),
545                "unexpected PascalCase key {legacy}; got {json}"
546            );
547        }
548    }
549
550    #[cfg(feature = "rust")]
551    #[test]
552    fn span_serializes_as_named_object() {
553        // The span is a flat named object preserving the 1-based
554        // tree-sitter line/column values in the original tuple order
555        // (start_line, start_col, end_line, end_col). The `*_line`
556        // vocabulary matches /function and /metrics (#638).
557        // `fn f(){}` is 8 bytes on one line, so the root spans the whole
558        // half-open byte range [0, 8) (#727).
559        let root = build_ast_with_span::<crate::RustParser>(b"fn f(){}", "test.rs");
560        let span = root
561            .span
562            .expect("root span present when span tracking is on");
563        assert_eq!(span, Span::new(1, 1, 1, 9, 0, 8));
564        let json = serde_json::to_string(&root.span).expect("serialize span");
565        assert!(
566            json.contains("\"start_line\":1")
567                && json.contains("\"start_col\":1")
568                && json.contains("\"end_line\":1")
569                && json.contains("\"end_col\":9")
570                && json.contains("\"start_byte\":0")
571                && json.contains("\"end_byte\":8"),
572            "expected named span object with byte offsets; got {json}"
573        );
574        // The pre-2.0 `*_row` keys must be gone (#638).
575        assert!(
576            !json.contains("start_row") && !json.contains("end_row"),
577            "unexpected pre-2.0 *_row span keys; got {json}"
578        );
579    }
580
581    #[test]
582    fn span_round_trips_through_serde() {
583        // Span derives Deserialize for wire round-trip parity.
584        let span = Span::new(2, 3, 4, 5, 11, 42);
585        let json = serde_json::to_string(&span).expect("serialize");
586        let back: Span = serde_json::from_str(&json).expect("deserialize");
587        assert_eq!(span, back);
588    }
589
590    #[test]
591    fn span_deserializes_pre_byte_offsets_with_default() {
592        // The byte fields carry `#[serde(default)]`, so a span object
593        // serialized before they existed (line/col only) still
594        // deserializes, with the offsets defaulting to 0 (#727).
595        let legacy = r#"{"start_line":2,"start_col":3,"end_line":4,"end_col":5}"#;
596        let span: Span = serde_json::from_str(legacy).expect("deserialize legacy span");
597        assert_eq!(span, Span::new(2, 3, 4, 5, 0, 0));
598    }
599
600    // -----------------------------------------------------------------
601    // Stack-depth regression tests (#1056)
602    // -----------------------------------------------------------------
603
604    /// A chain of `depth` nested [`AstNode`]s below the root, built
605    /// directly so the depth is exact rather than a function of how many
606    /// AST levels a grammar spends per source construct.
607    fn ast_chain(depth: usize) -> AstNode {
608        let mut node = AstNode::new("leaf", String::new(), None, Vec::new());
609        for _ in 0..depth {
610            node = AstNode::new("branch", String::new(), None, vec![node]);
611        }
612        node
613    }
614
615    #[test]
616    fn ast_depth_at_the_serialize_limit_is_accepted_and_one_deeper_is_not() {
617        let at_limit = ast_chain(MAX_AST_SERIALIZE_DEPTH);
618        serde_json::to_string(&at_limit).expect("the limit itself must serialize");
619
620        let past_limit = ast_chain(MAX_AST_SERIALIZE_DEPTH + 1);
621        let err = serde_json::to_string(&past_limit).expect_err("one deeper must be refused");
622        assert!(
623            err.to_string().contains(&format!(
624                "AstNode nesting is deeper than the serialization limit of \
625                 {MAX_AST_SERIALIZE_DEPTH} levels"
626            )),
627            "the error must name the type and the limit, got: {err}"
628        );
629    }
630
631    /// The deepest `children` chain in a tree, measured iteratively so a
632    /// pathological input cannot overflow the measurement itself.
633    #[cfg(feature = "tcl")]
634    fn ast_depth(root: &AstNode) -> usize {
635        let mut deepest = 0;
636        let mut stack = vec![(root, 1usize)];
637        while let Some((node, depth)) = stack.pop() {
638            deepest = deepest.max(depth);
639            stack.extend(node.children.iter().map(|child| (child, depth + 1)));
640        }
641        deepest
642    }
643
644    /// What one level of Tcl brace nesting costs in `AstNode` levels, and
645    /// therefore how far [`MAX_AST_SERIALIZE_DEPTH`] actually reaches for
646    /// this language.
647    ///
648    /// The bound's doc quotes a corpus figure of 188 levels, which is a
649    /// count of `AstNode` levels and not a conversion rate from source
650    /// nesting. Since #1381 a Tcl script body keeps its children, so the
651    /// two quantities came apart here more than anywhere else: this test
652    /// is what keeps the number in that doc honest, and what would notice
653    /// if an alterator change moved it again.
654    #[cfg(feature = "tcl")]
655    #[test]
656    fn a_tcl_brace_level_costs_three_ast_levels() {
657        // `eval` evaluates every argument, so each level keeps its
658        // children rather than flattening to a verbatim leaf.
659        let levels = 64;
660        let mut code = String::new();
661        for _ in 0..levels {
662            code.push_str("eval {");
663        }
664        code.push_str("puts hi");
665        for _ in 0..levels {
666            code.push('}');
667        }
668
669        let root = build_ast::<crate::TclParser>(code.as_bytes(), "deep.tcl");
670        let depth = ast_depth(&root);
671        let per_level = depth / levels;
672
673        // Measured 196 over 64 at the pinned grammar: `braced_word` ->
674        // `command` -> `word_list` before the next `braced_word`. The
675        // range admits one level of drift either way so a grammar bump
676        // reports the change rather than simply going red.
677        assert!(
678            (3..=4).contains(&per_level),
679            "a braced script level should cost 3 AstNode levels \
680             (braced_word -> command -> word_list); measured {depth} \
681             levels over {levels} braces = {per_level}"
682        );
683        // The consequence worth stating in one place: the 512-level bound
684        // is a low-hundreds brace ceiling for Tcl, not a 512 one.
685        let ceiling = MAX_AST_SERIALIZE_DEPTH / per_level;
686        assert!(
687            (120..=180).contains(&ceiling),
688            "the effective Tcl brace ceiling should sit near 170, got \
689             {ceiling} from {per_level} levels per brace"
690        );
691    }
692
693    #[test]
694    fn a_pathologically_deep_ast_errors_and_tears_down_without_overflowing() {
695        // The chain is built directly rather than parsed, but this depth
696        // is reachable through `bca-web`'s `/ast`: 50 000 levels of
697        // nested parentheses fit in 100 KB, an order of magnitude inside
698        // the 4 MiB body cap. Both the recursive `Serialize` and the
699        // compiler-generated `Drop` glue used to abort the process here —
700        // `SIGABRT`, which `spawn_blocking` cannot contain (#1056).
701        const DEPTH: usize = 50_000;
702        // The stack a `bca` consumer thread and a `tokio` blocking thread
703        // get; the serialization limit is dimensioned against it.
704        const PRODUCTION_STACK: usize = 2 * 1024 * 1024;
705        let message = std::thread::Builder::new()
706            .stack_size(PRODUCTION_STACK)
707            .spawn(|| {
708                let root = ast_chain(DEPTH);
709                let message = serde_json::to_string(&root)
710                    .expect_err("depth past the limit must fail, not serialize")
711                    .to_string();
712                // `root` drops here: the iterative `Drop` must unwind
713                // 50 000 levels without touching the stack.
714                message
715            })
716            .expect("spawn bounded-stack thread")
717            .join()
718            .expect("bounded-stack thread must not overflow");
719        assert!(
720            message.contains("AstNode nesting is deeper than the serialization limit"),
721            "the error must explain the refusal, got: {message}"
722        );
723    }
724}