Skip to main content

codehelion_core/
ir.rs

1//! The Syntax IR: a language-neutral structural view of one source file.
2//!
3//! Structural mode compares code by shape, not just by token content. Each
4//! frontend parses a file with its real parser and maps the resulting tree
5//! onto this IR: a token stream (the same [`Token`] representation Fast mode
6//! uses) plus a tree of [`IrNode`]s whose [`Shape`]s come from a small
7//! cross-language vocabulary. Nodes that have no cross-language equivalent
8//! keep their native grammar kind instead of being forced into the nearest
9//! common shape — a C++ `template_declaration` stays distinguishable from a
10//! Rust generic function.
11//!
12//! Error tolerance follows the frontend contract: a malformed region becomes
13//! an [`Shape::Error`] node covering its source range and parsing continues.
14//! Consumers that segment the tree into units must search recursively and
15//! judge each found node by its own subtree, never by the mere presence of an
16//! error ancestor: real parsers wrap large healthy regions in error nodes
17//! whose ranges are the union of individually intact children.
18//!
19//! Macros and templates are not expanded in Fast or Structural mode. The IR
20//! records definition sites ([`Shape::MacroDef`]) and invocation sites
21//! ([`Shape::MacroCall`]) as ordinary nodes so later phases can attach
22//! expansion information without changing this schema.
23//!
24//! Byte ranges are the only positions stored on nodes; line/column rendering
25//! is a reporting concern served by the token stream. No position feeds any
26//! stable identifier.
27//!
28//! # Schema versioning
29//!
30//! [`IR_SCHEMA_VERSION`] is a fingerprint input, and fingerprints built from
31//! different IR schema versions are never considered equal. It stays at 1
32//! until the first release tag: nothing has shipped, so a change that alters a
33//! comparison result — adding or removing a [`Shape`], changing a shape tag,
34//! changing how frontends map native kinds — invalidates the databases holding
35//! the old results rather than being versioned away from them, and re-running
36//! the scan is the whole of the recovery.
37
38use crate::discovery::Language;
39use crate::frontend::{Diagnostic, Lexeme, Token};
40
41/// Version of the Syntax IR schema, recorded per file and hashed into every
42/// structural fingerprint.
43pub const IR_SCHEMA_VERSION: u32 = 1;
44
45/// Maximum number of IR nodes on one root-to-leaf path emitted by the bundled
46/// structural frontends.
47///
48/// Frontends stop descending before this budget can be exceeded and preserve
49/// the omitted source region as an [`Shape::Error`] leaf. This keeps
50/// structural recovery bounded for mechanically generated and adversarial
51/// source files.
52pub const MAX_IR_DEPTH: usize = 500;
53
54/// A half-open byte range into the source text.
55///
56/// Ordering is by start then end, which is source order: it exists so ranges
57/// can be sorted into a deterministic reporting order, and carries no meaning
58/// beyond that.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
60pub struct ByteRange {
61    /// Byte offset of the range start.
62    pub start: usize,
63    /// Byte offset one past the range end.
64    pub end: usize,
65}
66
67impl ByteRange {
68    /// Length of the range in bytes; `0` for a malformed range.
69    #[must_use]
70    pub const fn len(&self) -> usize {
71        self.end.saturating_sub(self.start)
72    }
73
74    /// Whether the range covers no bytes.
75    #[must_use]
76    pub const fn is_empty(&self) -> bool {
77        self.len() == 0
78    }
79
80    /// Whether `other` lies entirely within this range.
81    #[must_use]
82    pub const fn contains(&self, other: &Self) -> bool {
83        self.start <= other.start && other.end <= self.end
84    }
85}
86
87/// The cross-language shape vocabulary.
88///
89/// Every variant except [`Shape::Native`] means the same thing in all three
90/// languages, so structural comparison across files (and, in later phases,
91/// across languages) can work on shapes alone. A frontend maps its grammar
92/// onto these; whatever does not fit is carried as [`Shape::Native`] with the
93/// grammar's own kind name preserved.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum Shape {
96    /// A free function definition.
97    Function,
98    /// A method definition (function inside an `impl`/class/struct body).
99    Method,
100    /// A closure or lambda.
101    Closure,
102    /// A record definition: `struct`, `class`, `union` or `enum`.
103    Record,
104    /// An implementation or member-definition container (`impl`, class body).
105    Impl,
106    /// A braced statement block.
107    Block,
108    /// A loop of any flavour (`for`, `while`, `loop`, do-while).
109    Loop,
110    /// A two-way conditional (`if`/`else` chain member).
111    Branch,
112    /// A multi-way conditional (`match`, `switch`).
113    Match,
114    /// One arm of a multi-way conditional.
115    MatchArm,
116    /// A function, method or macro-like call expression.
117    Call,
118    /// An assignment or compound assignment.
119    Assign,
120    /// A local variable declaration (`let`, C/C++ declaration statement).
121    VarDecl,
122    /// A `return` (or expression-position tail return).
123    Return,
124    /// An early loop exit (`break`).
125    Break,
126    /// A loop continuation (`continue`).
127    Continue,
128    /// Error propagation or handling (`?` operator, `try`/`catch`).
129    Try,
130    /// An expression used as a statement, not covered by a finer shape.
131    ExprStmt,
132    /// A macro or preprocessor definition site.
133    MacroDef,
134    /// A macro invocation site (not expanded).
135    MacroCall,
136    /// A region the parser could not interpret. Children may still be intact.
137    Error,
138    /// A node kept under its native grammar kind because no common shape
139    /// applies. The kind name takes part in structural comparison, so two
140    /// native nodes match only when their grammars call them the same thing.
141    Native(Lexeme),
142}
143
144impl Shape {
145    /// A stable one-byte tag for this shape, for use as fingerprint input.
146    ///
147    /// For [`Shape::Native`] the tag alone is not sufficient input: the
148    /// native kind name must be hashed alongside it, or all native nodes
149    /// would collapse into one shape.
150    #[must_use]
151    pub const fn tag(&self) -> u8 {
152        match self {
153            Self::Function => 1,
154            Self::Method => 2,
155            Self::Closure => 3,
156            Self::Record => 4,
157            Self::Impl => 5,
158            Self::Block => 6,
159            Self::Loop => 7,
160            Self::Branch => 8,
161            Self::Match => 9,
162            Self::MatchArm => 10,
163            Self::Call => 11,
164            Self::Assign => 12,
165            Self::VarDecl => 13,
166            Self::Return => 14,
167            Self::Break => 15,
168            Self::Continue => 16,
169            Self::Try => 17,
170            Self::ExprStmt => 18,
171            Self::MacroDef => 19,
172            Self::MacroCall => 20,
173            Self::Error => 21,
174            Self::Native(_) => 22,
175        }
176    }
177
178    /// Whether this shape opens a lexical scope for alpha renaming.
179    ///
180    /// This is the structural basis normalization uses to rename identifiers
181    /// consistently within — and only within — one scope. The judgement is
182    /// syntactic and shared by all three languages: bodies bind, containers
183    /// and single statements do not.
184    #[must_use]
185    pub const fn introduces_scope(&self) -> bool {
186        matches!(
187            self,
188            Self::Function
189                | Self::Method
190                | Self::Closure
191                | Self::Block
192                | Self::Loop
193                | Self::Branch
194                | Self::Match
195                | Self::MatchArm
196                | Self::Try
197        )
198    }
199
200    /// Whether nodes of this shape are statements for the purposes of
201    /// statement sequences and statement-window fragments.
202    #[must_use]
203    pub const fn is_statement(&self) -> bool {
204        matches!(
205            self,
206            Self::Loop
207                | Self::Branch
208                | Self::Match
209                | Self::Assign
210                | Self::VarDecl
211                | Self::Return
212                | Self::Break
213                | Self::Continue
214                | Self::Try
215                | Self::ExprStmt
216                | Self::MacroCall
217        )
218    }
219}
220
221/// One node of the Syntax IR tree.
222///
223/// A node covers a contiguous token range (`token_start..token_end` indices
224/// into [`SyntaxIrFile::tokens`]) and a contiguous byte range of the source.
225/// Children are in source order and lie within their parent's ranges. Nodes
226/// destroy their descendants iteratively so dropping a recovered file does not
227/// consume stack space proportional to its depth.
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct IrNode {
230    /// The node's shape.
231    pub shape: Shape,
232    /// The declared name, when the frontend can recover one (functions,
233    /// methods, records, macro definitions).
234    pub name: Option<Lexeme>,
235    /// Index of the node's first token in the file's token stream.
236    pub token_start: usize,
237    /// Index one past the node's last token.
238    pub token_end: usize,
239    /// Source bytes the node covers.
240    pub range: ByteRange,
241    /// Child nodes in source order.
242    pub children: Vec<Self>,
243}
244
245impl IrNode {
246    /// Number of tokens the node covers; `0` for a malformed range.
247    #[must_use]
248    pub const fn token_len(&self) -> usize {
249        self.token_end.saturating_sub(self.token_start)
250    }
251
252    /// Depth-first pre-order traversal over this node and its descendants.
253    ///
254    /// The traversal is iterative so callers can safely inspect externally
255    /// constructed IR too, even when it did not come from a bounded frontend.
256    pub fn walk(&self, visit: &mut impl FnMut(&Self)) {
257        let mut pending = vec![self];
258        while let Some(node) = pending.pop() {
259            visit(node);
260            pending.extend(node.children.iter().rev());
261        }
262    }
263
264    /// The sequence of statement children, summarised.
265    ///
266    /// This is the per-block statement sequence view of the IR: direct
267    /// children whose shapes are statements, in source order, each reduced
268    /// to its [`StatementSummary`]. Non-statement children (nested items,
269    /// blocks acting as expressions) are skipped, matching how statement
270    /// windows are cut.
271    ///
272    /// [`Shape::Native`] children are included: a native node that is a
273    /// direct child of the node being summarised sits in statement position
274    /// by construction (C `goto`, a preprocessor conditional inside a
275    /// function body), and dropping it would silently shorten the sequence.
276    #[must_use]
277    pub fn statement_summaries(&self, tokens: &[Token]) -> Vec<StatementSummary> {
278        self.children
279            .iter()
280            .filter(|child| child.shape.is_statement() || matches!(child.shape, Shape::Native(_)))
281            .map(|child| StatementSummary::of(child, tokens))
282            .collect()
283    }
284}
285
286impl Drop for IrNode {
287    fn drop(&mut self) {
288        let mut worklist = std::mem::take(&mut self.children);
289        while let Some(mut node) = worklist.pop() {
290            worklist.append(&mut node.children);
291        }
292    }
293}
294
295/// How many leading tokens a [`StatementSummary`] keeps.
296pub const SUMMARY_HEAD_TOKENS: usize = 4;
297
298/// A statement reduced to its shape and the span of its tokens.
299///
300/// The shape carries the rename-invariant signal that aligns two statement
301/// sequences; the span is how the text itself is recovered, for the lexical
302/// comparison that decides whether aligned statements are actually copies.
303///
304/// The span is kept rather than the token texts because a compound statement
305/// covers its whole body: cloning the texts would cost a copy of the token
306/// stream once per level of nesting, while an index pair costs the same
307/// whatever the statement contains. It is a position into one file's stream
308/// and nothing more — identity in this tool is content-derived, and no
309/// fingerprint reads this type.
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct StatementSummary {
312    /// Shape tag of the statement (see [`Shape::tag`]).
313    pub shape_tag: u8,
314    /// Native kind name when the statement is a [`Shape::Native`] node.
315    pub native_kind: Option<Lexeme>,
316    /// Index of the statement's first token in its file's stream.
317    pub token_start: usize,
318    /// Index one past the statement's last token in its file's stream.
319    pub token_end: usize,
320}
321
322impl StatementSummary {
323    /// Summarise one node against its file's token stream.
324    #[must_use]
325    pub fn of(node: &IrNode, tokens: &[Token]) -> Self {
326        let native_kind = match &node.shape {
327            Shape::Native(kind) => Some(kind.clone()),
328            _ => None,
329        };
330        let token_end = node.token_end.min(tokens.len());
331        Self {
332            shape_tag: node.shape.tag(),
333            native_kind,
334            token_start: node.token_start.min(token_end),
335            token_end,
336        }
337    }
338
339    /// The statement's tokens, resolved against the stream it was summarised
340    /// from. Empty for any other stream, since the span would not be its own.
341    #[must_use]
342    pub fn tokens<'a>(&self, tokens: &'a [Token]) -> &'a [Token] {
343        tokens.get(self.token_start..self.token_end).unwrap_or(&[])
344    }
345}
346
347/// The Syntax IR of one source file.
348#[derive(Debug, Clone)]
349pub struct SyntaxIrFile {
350    /// Language the file was parsed as.
351    pub language: Language,
352    /// Version tag of the structural frontend that produced this IR; a
353    /// fingerprint input alongside [`IR_SCHEMA_VERSION`].
354    pub frontend_version: &'static str,
355    /// IR schema version this file conforms to.
356    pub ir_schema_version: u32,
357    /// Tokens in source order, comments and whitespace removed. Same
358    /// representation as Fast mode, so token-level normalization is shared.
359    pub tokens: Vec<Token>,
360    /// Top-level IR nodes in source order.
361    pub roots: Vec<IrNode>,
362    /// Recoverable lexical problems, as in Fast mode.
363    pub diagnostics: Vec<Diagnostic>,
364    /// Source regions the parser marked as errors. Overlapping nodes are
365    /// still emitted; these ranges only lower confidence downstream.
366    pub error_ranges: Vec<ByteRange>,
367    /// Whether the frontend stopped descending after reaching its structural
368    /// depth budget.
369    ///
370    /// This is deliberately separate from [`Self::error_ranges`]: malformed
371    /// source is ordinary recovery, while a depth ceiling means the scan was
372    /// intentionally incomplete and must be reported as such.
373    pub depth_truncated: bool,
374    /// Whether the file is the body of a module its tree declares test-only.
375    ///
376    /// Not something a parse can answer: the declaration carrying the marker
377    /// is in another file, so this is settled once the whole set is known and
378    /// left here for the walk that reads a unit's markers to start from. A
379    /// frontend leaves it false. See
380    /// [`declared_test_modules`](crate::test_code::declared_test_modules).
381    pub test_module: bool,
382}
383
384impl SyntaxIrFile {
385    /// Depth-first pre-order traversal over every node in the file.
386    pub fn walk(&self, visit: &mut impl FnMut(&IrNode)) {
387        for root in &self.roots {
388            root.walk(visit);
389        }
390    }
391
392    /// Total number of nodes in the file.
393    #[must_use]
394    pub fn node_count(&self) -> usize {
395        let mut count = 0;
396        self.walk(&mut |_| count += 1);
397        count
398    }
399
400    /// Tokens the parser could not attach to any structure: those inside a
401    /// [`Shape::Error`] node that none of its children recovered.
402    ///
403    /// This is the honest measure of what a parse lost, and
404    /// [`error_ranges`](Self::error_ranges) is not. An error-tolerant parser
405    /// recovering from one bad construct routinely wraps everything around it
406    /// in a single error node: a header whose include guard encloses the file
407    /// gets one error region covering every byte of it, with the whole file's
408    /// declarations intact inside. Measured over one project's C++ sources,
409    /// the error regions covered 13.3% of the bytes while the tokens that
410    /// actually failed to parse were 1.82% — the difference is entirely code
411    /// the parser did read, sitting inside a region it had to open.
412    ///
413    /// Nesting is not double-counted: an error node inside another is covered
414    /// by its parent's children, so the parent contributes only the gaps
415    /// around it and the child contributes its own.
416    #[must_use]
417    pub fn unaccounted_tokens(&self) -> usize {
418        let mut lost = 0;
419        self.walk(&mut |node| {
420            if matches!(node.shape, Shape::Error) {
421                let recovered: usize = node.children.iter().map(IrNode::token_len).sum();
422                lost += node.token_len().saturating_sub(recovered);
423            }
424        });
425        lost
426    }
427}
428
429/// A Structural-mode parser for one language.
430///
431/// Like the Fast [`Frontend`](crate::frontend::Frontend), a structural
432/// frontend never executes, expands or resolves anything in the target code:
433/// it parses text and maps the tree. Malformed input degrades to
434/// [`Shape::Error`] nodes plus [`SyntaxIrFile::error_ranges`]. A frontend
435/// that reaches its depth budget records the remaining source region in the
436/// same way instead of continuing recursive descent.
437pub trait StructuralFrontend {
438    /// The language this frontend parses.
439    fn language(&self) -> Language;
440
441    /// The frontend's version tag, used as a fingerprint input.
442    fn frontend_version(&self) -> &'static str;
443
444    /// Parse `source` into a Syntax IR file.
445    fn parse(&self, source: &str) -> SyntaxIrFile;
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use crate::frontend::{SourceSpan, TokenKind};
452
453    fn token(text: &str, start_byte: usize) -> Token {
454        Token {
455            kind: TokenKind::Identifier,
456            text: Lexeme::from(text),
457            span: SourceSpan {
458                start_byte,
459                end_byte: start_byte + text.len(),
460                start_line: 1,
461                start_column: 1,
462            },
463        }
464    }
465
466    fn node(shape: Shape, token_start: usize, token_end: usize) -> IrNode {
467        IrNode {
468            shape,
469            name: None,
470            token_start,
471            token_end,
472            range: ByteRange {
473                start: token_start,
474                end: token_end,
475            },
476            children: Vec::new(),
477        }
478    }
479
480    #[test]
481    fn dropping_a_deep_tree_is_iterative() {
482        let mut tree = node(Shape::Block, 0, 0);
483        for _ in 0..10_000 {
484            tree = IrNode {
485                shape: Shape::Block,
486                name: None,
487                token_start: 0,
488                token_end: 0,
489                range: ByteRange { start: 0, end: 0 },
490                children: vec![tree],
491            };
492        }
493
494        drop(tree);
495    }
496
497    #[test]
498    fn shape_tags_are_distinct_and_stable() {
499        let shapes = [
500            Shape::Function,
501            Shape::Method,
502            Shape::Closure,
503            Shape::Record,
504            Shape::Impl,
505            Shape::Block,
506            Shape::Loop,
507            Shape::Branch,
508            Shape::Match,
509            Shape::MatchArm,
510            Shape::Call,
511            Shape::Assign,
512            Shape::VarDecl,
513            Shape::Return,
514            Shape::Break,
515            Shape::Continue,
516            Shape::Try,
517            Shape::ExprStmt,
518            Shape::MacroDef,
519            Shape::MacroCall,
520            Shape::Error,
521            Shape::Native(Lexeme::from("preproc_ifdef")),
522        ];
523        let mut tags: Vec<u8> = shapes.iter().map(Shape::tag).collect();
524        tags.sort_unstable();
525        tags.dedup();
526        assert_eq!(tags.len(), shapes.len(), "shape tags must be distinct");
527        // Tag values are part of the fingerprint schema: spot-pin endpoints.
528        assert_eq!(Shape::Function.tag(), 1);
529        assert_eq!(Shape::Native(Lexeme::from("x")).tag(), 22);
530    }
531
532    #[test]
533    fn native_nodes_share_a_tag_but_keep_their_kind() {
534        let a = Shape::Native(Lexeme::from("preproc_ifdef"));
535        let b = Shape::Native(Lexeme::from("using_declaration"));
536        assert_eq!(a.tag(), b.tag());
537        assert_ne!(a, b, "the kind name still distinguishes native shapes");
538    }
539
540    #[test]
541    fn scope_and_statement_tables_are_consistent() {
542        assert!(Shape::Function.introduces_scope());
543        assert!(Shape::Block.introduces_scope());
544        assert!(!Shape::Call.introduces_scope());
545        assert!(!Shape::Record.introduces_scope());
546
547        assert!(Shape::Return.is_statement());
548        assert!(Shape::MacroCall.is_statement());
549        assert!(!Shape::Function.is_statement(), "items are not statements");
550        assert!(!Shape::Block.is_statement());
551    }
552
553    #[test]
554    fn byte_range_arithmetic_guards_malformed_input() {
555        let range = ByteRange { start: 10, end: 20 };
556        assert_eq!(range.len(), 10);
557        assert!(!range.is_empty());
558        assert!(range.contains(&ByteRange { start: 12, end: 18 }));
559        assert!(!range.contains(&ByteRange { start: 5, end: 18 }));
560
561        let malformed = ByteRange { start: 20, end: 10 };
562        assert_eq!(malformed.len(), 0);
563        assert!(malformed.is_empty());
564    }
565
566    #[test]
567    fn statement_summaries_take_statement_children_in_order() {
568        let tokens: Vec<Token> = ["let", "x", "=", "f", "(", ")", "return", "x"]
569            .iter()
570            .enumerate()
571            .map(|(i, text)| token(text, i * 8))
572            .collect();
573
574        let mut block = node(Shape::Block, 0, 8);
575        block.children = vec![
576            node(Shape::VarDecl, 0, 6),
577            node(Shape::Function, 0, 0), // nested item: not a statement
578            node(Shape::Return, 6, 8),
579        ];
580
581        let summaries = block.statement_summaries(&tokens);
582        assert_eq!(summaries.len(), 2);
583        assert!(
584            summaries.iter().all(|s| s.native_kind.is_none()),
585            "no native statements in this block"
586        );
587        assert_eq!(summaries[0].shape_tag, Shape::VarDecl.tag());
588        let text = |summary: &StatementSummary| -> Vec<String> {
589            summary
590                .tokens(&tokens)
591                .iter()
592                .map(|token| token.text.as_str().to_string())
593                .collect()
594        };
595        assert_eq!(
596            text(&summaries[0]),
597            vec!["let", "x", "=", "f", "(", ")"],
598            "the span covers the whole statement, not just its head"
599        );
600        assert_eq!(summaries[1].shape_tag, Shape::Return.tag());
601        assert_eq!(text(&summaries[1]), vec!["return", "x"]);
602    }
603
604    #[test]
605    fn native_children_count_as_statements_in_position() {
606        let tokens = vec![token("goto", 0), token("fail", 8)];
607        let mut block = node(Shape::Block, 0, 2);
608        block.children = vec![IrNode {
609            shape: Shape::Native(Lexeme::from("goto_statement")),
610            name: None,
611            token_start: 0,
612            token_end: 2,
613            range: ByteRange { start: 0, end: 12 },
614            children: Vec::new(),
615        }];
616
617        let summaries = block.statement_summaries(&tokens);
618        assert_eq!(summaries.len(), 1);
619        assert_eq!(
620            summaries[0].native_kind,
621            Some(Lexeme::from("goto_statement"))
622        );
623    }
624
625    #[test]
626    fn summary_of_out_of_bounds_token_range_is_empty_not_panicking() {
627        let tokens = vec![token("x", 0)];
628        let stray = node(Shape::ExprStmt, 5, 9);
629        let summary = StatementSummary::of(&stray, &tokens);
630        assert!(summary.tokens(&tokens).is_empty());
631    }
632
633    #[test]
634    fn walk_visits_every_node_pre_order() {
635        let mut root = node(Shape::Function, 0, 10);
636        let mut block = node(Shape::Block, 1, 9);
637        block.children = vec![node(Shape::Return, 2, 4)];
638        root.children = vec![block];
639
640        let file = SyntaxIrFile {
641            language: Language::Rust,
642            frontend_version: "test-v1",
643            ir_schema_version: IR_SCHEMA_VERSION,
644            tokens: Vec::new(),
645            roots: vec![root],
646            diagnostics: Vec::new(),
647            error_ranges: Vec::new(),
648            depth_truncated: false,
649            test_module: false,
650        };
651
652        let mut seen = Vec::new();
653        file.walk(&mut |n| seen.push(n.shape.tag()));
654        assert_eq!(
655            seen,
656            vec![
657                Shape::Function.tag(),
658                Shape::Block.tag(),
659                Shape::Return.tag()
660            ]
661        );
662        assert_eq!(file.node_count(), 3);
663    }
664
665    /// A file whose roots are `roots`, for the traversal tests.
666    fn file_of(roots: Vec<IrNode>) -> SyntaxIrFile {
667        SyntaxIrFile {
668            language: Language::Rust,
669            frontend_version: "test-v1",
670            ir_schema_version: IR_SCHEMA_VERSION,
671            tokens: Vec::new(),
672            roots,
673            diagnostics: Vec::new(),
674            error_ranges: Vec::new(),
675            depth_truncated: false,
676            test_module: false,
677        }
678    }
679
680    #[test]
681    fn code_recovered_inside_an_error_node_is_not_counted_as_lost() {
682        // The shape an error-tolerant parser actually produces: one error
683        // node opened by a construct it could not read, holding everything
684        // that followed and parsed cleanly. Counting the node's own extent
685        // would call the whole file unreadable.
686        let mut wrapper = node(Shape::Error, 0, 100);
687        wrapper.children = vec![node(Shape::Function, 3, 60), node(Shape::Function, 60, 100)];
688        assert_eq!(
689            file_of(vec![wrapper]).unaccounted_tokens(),
690            3,
691            "only the tokens no child accounts for"
692        );
693    }
694
695    #[test]
696    fn an_error_node_that_recovered_nothing_loses_all_of_it() {
697        assert_eq!(
698            file_of(vec![node(Shape::Error, 0, 40)]).unaccounted_tokens(),
699            40
700        );
701    }
702
703    #[test]
704    fn a_file_the_parser_followed_loses_nothing() {
705        let mut function = node(Shape::Function, 0, 20);
706        function.children = vec![node(Shape::Block, 4, 20)];
707        assert_eq!(file_of(vec![function]).unaccounted_tokens(), 0);
708    }
709
710    #[test]
711    fn nested_error_nodes_count_their_own_gaps_once() {
712        // The inner error is one of the outer's children, so the outer counts
713        // only what surrounds it and the inner counts what it failed to
714        // recover. Adding both extents would report more than the file holds.
715        let mut inner = node(Shape::Error, 40, 60);
716        inner.children = vec![node(Shape::Return, 45, 55)];
717        let mut outer = node(Shape::Error, 0, 100);
718        outer.children = vec![node(Shape::Function, 0, 40), inner];
719        assert_eq!(
720            file_of(vec![outer]).unaccounted_tokens(),
721            40 + 10,
722            "the outer's trailing gap plus the inner's own"
723        );
724    }
725}