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 tokens do not form an expression constraint.
30    #[error("expected {expected} at byte {offset}, found {found}")]
31    Syntax {
32        /// The byte offset where the parser stopped.
33        offset: usize,
34        /// The token class the grammar admits there.
35        expected: String,
36        /// The text found there, or "the end of the expression".
37        found: String,
38    },
39}
40
41impl ParseError {
42    /// The byte offset the error points at.
43    #[must_use]
44    pub fn offset(&self) -> usize {
45        match self {
46            Self::Lex(error) => error.offset,
47            Self::Syntax { offset, .. } => *offset,
48        }
49    }
50}
51
52/// Parses an expression constraint.
53///
54/// # Errors
55///
56/// Returns [`ParseError`] with the byte offset of the first character or
57/// token the grammar does not admit. An identifier that names no concept
58/// still parses; the evaluator refuses it.
59///
60/// # Examples
61///
62/// ```
63/// let tree = sct_ecl::parse("<< 73211009 |Diabetes mellitus|")?;
64/// assert_eq!(tree.to_string(), "<< 73211009 |Diabetes mellitus|");
65/// # Ok::<(), sct_ecl::ParseError>(())
66/// ```
67pub fn parse(input: &str) -> Result<ExpressionConstraint, ParseError> {
68    let tokens = lexer::lex(input)?;
69    parser::whole
70        .parse(TokenSlice::new(&tokens))
71        .map_err(|error| {
72            let index = error.offset();
73            let (offset, found) = tokens.get(index).map_or_else(
74                || (input.len(), String::from("the end of the expression")),
75                |token| (token.span.start, format!("{:?}", token.text)),
76            );
77            ParseError::Syntax {
78                offset,
79                expected: String::from(
80                    error
81                        .inner()
82                        .expected
83                        .unwrap_or("a valid expression constraint"),
84                ),
85                found,
86            }
87        })
88}
89
90impl std::str::FromStr for ExpressionConstraint {
91    type Err = ParseError;
92
93    fn from_str(s: &str) -> Result<Self, Self::Err> {
94        parse(s)
95    }
96}