Skip to main content

antlr4_runtime/
validated.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (c) 2026 Konstantin Vyatkin
3//! Shared validated-parse surface for generated recognizers.
4//!
5//! Every generated parser exposes a strict parsing mode that rejects syntax
6//! errors, recovered error nodes, and missing generated required children
7//! before handing out a tree whose typed accessors are infallible. The types
8//! backing that surface are grammar-agnostic — all grammar-specific
9//! information they carry (context and child names) arrives as data from the
10//! generated `validate_tree_structure` — so they are defined once here and
11//! aliased by generated modules.
12//!
13//! [`ValidatedTree`] and [`ValidatedRuleNode`] are branded with a `Grammar`
14//! type parameter. Each generated module instantiates them with its
15//! module-local `ValidatedTreeContext` marker
16//! (`pub type TomlValidatedTree = antlr4_runtime::ValidatedTree<ValidatedTreeContext>;`),
17//! so trees and nodes of different grammars remain distinct types and
18//! [`ValidatedRuleNode::downcast_ref`] cannot resolve a node against another
19//! grammar's contexts, whose rule indexes and context kinds are grammar-local
20//! numbers. [`ValidationError`] is deliberately unbranded: a binary linking
21//! several generated parsers handles one error type and compiles one copy of
22//! its `Display`/`Error`/`From` machinery.
23
24use std::marker::PhantomData;
25
26use thiserror::Error;
27
28use crate::errors::AntlrError;
29use crate::tree::{MissingChildError, Node, ParsedFile, RuleNodeView};
30
31/// A completed, syntax-clean parse tree whose generated child cardinalities
32/// have been structurally validated.
33///
34/// Constructed only by a generated parser's `validate()` /
35/// `parse_validated()` conveniences after `validate_tree_structure` proved
36/// the required-child invariants, so [`ValidatedTree::tree`] and the
37/// validated context accessors never observe a violated invariant. `Grammar`
38/// is the generated module's `ValidatedTreeContext` marker; it keeps the
39/// validated trees of different grammars nominally distinct.
40pub struct ValidatedTree<Grammar> {
41    parsed: ParsedFile,
42    grammar: PhantomData<Grammar>,
43}
44
45impl<Grammar> ValidatedTree<Grammar> {
46    /// Wraps a parse whose structure was already validated.
47    ///
48    /// This is a doc-hidden contract for generated code, not a sealed
49    /// boundary: it is technically callable from any crate, and wrapping a
50    /// parse that did not pass the grammar's `validate_tree_structure` makes
51    /// later infallible validated accessors panic via `unreachable!`. (In
52    /// generated-code API revisions 9 and earlier the equivalent constructor
53    /// was private to the generated module.)
54    #[doc(hidden)]
55    #[must_use]
56    pub const fn __new(parsed: ParsedFile) -> Self {
57        Self {
58            parsed,
59            grammar: PhantomData,
60        }
61    }
62
63    /// Returns the validated entry-rule root.
64    #[must_use]
65    pub fn tree(&self) -> ValidatedRuleNode<'_, Grammar> {
66        let Some(rule) = self.parsed.tree().as_rule() else {
67            unreachable!("validated parse root was checked as a rule node")
68        };
69        ValidatedRuleNode {
70            node: rule,
71            grammar: PhantomData,
72        }
73    }
74
75    /// Borrows the underlying recovery-oriented parsed file.
76    #[must_use]
77    pub const fn parsed_file(&self) -> &ParsedFile {
78        &self.parsed
79    }
80
81    /// Drops the validation type boundary and returns the underlying parsed
82    /// file.
83    #[must_use]
84    pub fn into_parsed_file(self) -> ParsedFile {
85        self.parsed
86    }
87}
88
89impl<Grammar> std::fmt::Debug for ValidatedTree<Grammar> {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.debug_struct("ValidatedTree")
92            .field("parsed", &self.parsed)
93            .finish()
94    }
95}
96
97/// A rule node borrowed from a [`ValidatedTree`] with the same `Grammar`
98/// brand.
99pub struct ValidatedRuleNode<'a, Grammar> {
100    node: RuleNodeView<'a>,
101    grammar: PhantomData<Grammar>,
102}
103
104impl<'a, Grammar> ValidatedRuleNode<'a, Grammar> {
105    /// Wraps a rule node that belongs to an already-validated tree.
106    ///
107    /// This is a doc-hidden contract for generated code (validated walkers
108    /// and visitor bridges), not a sealed boundary: it is technically
109    /// callable from any crate, and minting a validated node over an
110    /// unvalidated tree makes later infallible validated accessors panic via
111    /// `unreachable!`. (In generated-code API revisions 9 and earlier the
112    /// node's field was private to the generated module.)
113    #[doc(hidden)]
114    #[must_use]
115    pub const fn __new(node: RuleNodeView<'a>) -> Self {
116        Self {
117            node,
118            grammar: PhantomData,
119        }
120    }
121
122    #[must_use]
123    pub const fn rule_node(self) -> RuleNodeView<'a> {
124        self.node
125    }
126
127    #[must_use]
128    pub const fn node(self) -> Node<'a> {
129        self.node.node()
130    }
131
132    #[must_use]
133    pub fn rule_index(self) -> usize {
134        self.node.rule_index()
135    }
136
137    #[must_use]
138    pub fn text(self) -> String {
139        self.node.text()
140    }
141
142    /// Views this node as one of the grammar's validated context types.
143    ///
144    /// The `Grammar` brand ties candidates to the grammar that produced the
145    /// node, so contexts of other generated parsers do not satisfy the bound.
146    #[must_use]
147    pub fn downcast_ref<T: FromValidatedRuleNode<'a, Grammar = Grammar>>(self) -> Option<T> {
148        T::from_validated_rule_node(self)
149    }
150}
151
152impl<Grammar> std::fmt::Debug for ValidatedRuleNode<'_, Grammar> {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        f.debug_struct("ValidatedRuleNode")
155            .field("node", &self.node)
156            .finish()
157    }
158}
159
160impl<Grammar> Clone for ValidatedRuleNode<'_, Grammar> {
161    fn clone(&self) -> Self {
162        *self
163    }
164}
165
166impl<Grammar> Copy for ValidatedRuleNode<'_, Grammar> {}
167
168/// Constructs a generated validated context from a validated rule node of
169/// the same grammar.
170pub trait FromValidatedRuleNode<'a>: Sized {
171    /// The generated module's `ValidatedTreeContext` marker.
172    type Grammar;
173
174    fn from_validated_rule_node(node: ValidatedRuleNode<'a, Self::Grammar>) -> Option<Self>;
175}
176
177/// Failure to recognize or validate a strict generated parse.
178///
179/// Grammar-specific detail (context and child names) is carried as variant
180/// data supplied by the generated `validate_tree_structure`, so one error
181/// type serves every generated parser.
182#[derive(Clone, Debug, Eq, PartialEq, Error)]
183pub enum ValidationError {
184    #[error("parse failed: {0}")]
185    Recognition(#[from] AntlrError),
186    #[error("parse produced {lexer} lexer and {parser} parser syntax errors")]
187    SyntaxErrors { lexer: usize, parser: usize },
188    #[error("{0}")]
189    MissingChild(#[from] MissingChildError),
190    #[error(
191        "required child {child} occurs {actual} times in {context}; expected at least {minimum}"
192    )]
193    InvalidChildCount {
194        context: &'static str,
195        child: &'static str,
196        minimum: usize,
197        actual: usize,
198    },
199    #[error("recovered error node at {line}:{column}: {text}")]
200    RecoveredErrorNode {
201        line: usize,
202        column: usize,
203        text: String,
204    },
205    #[error("validated parse root is not a rule node")]
206    InvalidRoot,
207    #[error("parse tree contains unknown rule index {rule_index}")]
208    UnknownRule { rule_index: usize },
209}
210
211/// Checks one generated repeated-child minimum-cardinality invariant.
212///
213/// Generated `validate_tree_structure` implementations call this once per
214/// required list child; `context` and `child` are grammar data supplied by
215/// the generated caller.
216///
217/// # Errors
218///
219/// Returns [`ValidationError::InvalidChildCount`] when `actual < minimum`.
220pub const fn require_min_count(
221    actual: usize,
222    minimum: usize,
223    context: &'static str,
224    child: &'static str,
225) -> Result<(), ValidationError> {
226    if actual < minimum {
227        return Err(ValidationError::InvalidChildCount {
228            context,
229            child,
230            minimum,
231            actual,
232        });
233    }
234    Ok(())
235}
236
237#[cfg(test)]
238#[allow(clippy::disallowed_methods)] // insta assertion macros unwrap internal I/O.
239mod tests {
240    use std::error::Error as _;
241
242    use super::*;
243
244    fn every_variant() -> Vec<ValidationError> {
245        vec![
246            ValidationError::Recognition(AntlrError::LexerError {
247                line: 3,
248                column: 7,
249                message: "token recognition error at: '#'".to_owned(),
250            }),
251            ValidationError::SyntaxErrors {
252                lexer: 1,
253                parser: 2,
254            },
255            ValidationError::MissingChild(MissingChildError::new("StartContext", "atom")),
256            ValidationError::InvalidChildCount {
257                context: "StartContext",
258                child: "atom",
259                minimum: 2,
260                actual: 1,
261            },
262            ValidationError::RecoveredErrorNode {
263                line: 4,
264                column: 9,
265                text: "<missing ';'>".to_owned(),
266            },
267            ValidationError::InvalidRoot,
268            ValidationError::UnknownRule { rule_index: 41 },
269        ]
270    }
271
272    #[test]
273    fn validation_error_display_texts() {
274        let rendered = every_variant()
275            .iter()
276            .map(ToString::to_string)
277            .collect::<Vec<_>>()
278            .join("\n");
279        insta::assert_snapshot!("validation_error_display_texts", rendered);
280    }
281
282    #[test]
283    fn validation_error_sources() {
284        for error in every_variant() {
285            let expects_source = matches!(
286                error,
287                ValidationError::Recognition(_) | ValidationError::MissingChild(_)
288            );
289            assert_eq!(
290                error.source().is_some(),
291                expects_source,
292                "source() mismatch for {error:?}"
293            );
294        }
295    }
296
297    #[test]
298    fn validation_error_from_conversions() {
299        let recognition = AntlrError::LexerError {
300            line: 1,
301            column: 0,
302            message: "boom".to_owned(),
303        };
304        assert_eq!(
305            ValidationError::from(recognition.clone()),
306            ValidationError::Recognition(recognition)
307        );
308
309        let missing = MissingChildError::new("StartContext", "atom");
310        assert_eq!(
311            ValidationError::from(missing),
312            ValidationError::MissingChild(missing)
313        );
314    }
315
316    #[test]
317    fn require_min_count_accepts_satisfied_minimums() {
318        assert_eq!(require_min_count(2, 2, "StartContext", "atom"), Ok(()));
319        assert_eq!(require_min_count(3, 0, "StartContext", "atom"), Ok(()));
320    }
321
322    #[test]
323    fn require_min_count_reports_the_violated_site() {
324        assert_eq!(
325            require_min_count(1, 2, "StartContext", "atom"),
326            Err(ValidationError::InvalidChildCount {
327                context: "StartContext",
328                child: "atom",
329                minimum: 2,
330                actual: 1,
331            })
332        );
333    }
334}