Skip to main content

sct_ecl/
lib.rs

1//! The Expression Constraint Language.
2//!
3//! A `logos` lexer and a `winnow` parser faithful to the official ECL ANTLR
4//! grammar (`vendor/syntax/ECL.g4`, the pinned tag in `docs/VERSIONS.md`;
5//! <https://docs.snomed.org/snomed-ct-specifications/snomed-ct-expression-constraint-language>),
6//! a syntax tree named after the grammar's rules, and a printer whose output
7//! parses back to the same tree.
8#![doc(test(attr(deny(warnings))))]
9
10pub mod ast;
11pub mod dialects;
12pub mod eval;
13pub mod lexer;
14pub mod parser;
15mod print;
16
17use winnow::prelude::*;
18use winnow::stream::TokenSlice;
19
20use crate::ast::ExpressionConstraint;
21use crate::lexer::LexError;
22
23/// A malformed expression constraint.
24#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25pub enum ParseError {
26    /// A character no token starts with.
27    #[error(transparent)]
28    Lex(#[from] LexError),
29    /// The expression nests deeper than the parser will descend.
30    #[error("the expression nests {depth} deep at byte {offset}; the limit is {limit}")]
31    TooDeep {
32        /// The byte offset of the bracket that crossed the limit.
33        offset: usize,
34        /// The nesting depth reached there.
35        depth: usize,
36        /// The deepest nesting the parser admits.
37        limit: usize,
38    },
39    /// The tokens do not form an expression constraint.
40    #[error("expected {expected} at byte {offset}, found {found}")]
41    Syntax {
42        /// The byte offset where the parser stopped.
43        offset: usize,
44        /// The token class the grammar admits there.
45        expected: String,
46        /// The text found there, or "the end of the expression".
47        found: String,
48    },
49}
50
51impl ParseError {
52    /// The byte offset the error points at.
53    #[must_use]
54    pub fn offset(&self) -> usize {
55        match self {
56            Self::Lex(error) => error.offset,
57            Self::TooDeep { offset, .. } | Self::Syntax { offset, .. } => *offset,
58        }
59    }
60}
61
62/// The deepest nesting of `(`, `{`, `{{`, or `[` an expression may carry.
63///
64/// No specification governs this: our own design. The grammar is recursive and
65/// the parser descends with it, so nesting costs stack; an expression a client
66/// sends is refused here rather than run until the process aborts, which no
67/// `OperationOutcome` survives. Real expressions nest a handful deep.
68pub const NESTING_LIMIT: usize = 64;
69
70/// The byte offset and depth of the first bracket past [`NESTING_LIMIT`].
71fn too_deep(tokens: &[lexer::Token<'_>]) -> Option<(usize, usize)> {
72    let mut depth = 0_usize;
73    for token in tokens {
74        match token.kind {
75            lexer::Kind::LeftParen
76            | lexer::Kind::LeftBrace
77            | lexer::Kind::DoubleLeftBrace
78            | lexer::Kind::LeftBracket => {
79                depth = depth.saturating_add(1);
80                if depth > NESTING_LIMIT {
81                    return Some((token.span.start, depth));
82                }
83            }
84            lexer::Kind::RightParen
85            | lexer::Kind::RightBrace
86            | lexer::Kind::DoubleRightBrace
87            | lexer::Kind::RightBracket => depth = depth.saturating_sub(1),
88            _ => {}
89        }
90    }
91    None
92}
93
94/// Parses an expression constraint.
95///
96/// # Errors
97///
98/// Returns [`ParseError`] with the byte offset of the first character or
99/// token the grammar does not admit. An identifier that names no concept
100/// still parses; the evaluator refuses it.
101///
102/// # Examples
103///
104/// ```
105/// let tree = sct_ecl::parse("<< 73211009 |Diabetes mellitus|")?;
106/// assert_eq!(tree.to_string(), "<< 73211009 |Diabetes mellitus|");
107/// # Ok::<(), sct_ecl::ParseError>(())
108/// ```
109pub fn parse(input: &str) -> Result<ExpressionConstraint, ParseError> {
110    let tokens = lexer::lex(input)?;
111    if let Some((offset, depth)) = too_deep(&tokens) {
112        return Err(ParseError::TooDeep {
113            offset,
114            depth,
115            limit: NESTING_LIMIT,
116        });
117    }
118    parser::whole
119        .parse(TokenSlice::new(&tokens))
120        .map_err(|error| {
121            let index = error.offset();
122            let (offset, found) = tokens.get(index).map_or_else(
123                || (input.len(), String::from("the end of the expression")),
124                |token| (token.span.start, format!("{:?}", token.text)),
125            );
126            ParseError::Syntax {
127                offset,
128                expected: String::from(
129                    error
130                        .inner()
131                        .expected
132                        .unwrap_or("a valid expression constraint"),
133                ),
134                found,
135            }
136        })
137}
138
139impl std::str::FromStr for ExpressionConstraint {
140    type Err = ParseError;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        parse(s)
144    }
145}