Skip to main content

sct_ecl/
lib.rs

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