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}