Skip to main content

snomed_ecl_engine/ecl/
refinement.rs

1use super::*;
2use crate::decimal::Decimal;
3use std::cmp::Ordering;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub struct Cardinality {
7    pub min: u64,
8    pub max: Option<u64>,
9}
10impl Default for Cardinality {
11    fn default() -> Self {
12        Self { min: 1, max: None }
13    }
14}
15impl Cardinality {
16    pub fn contains(self, count: usize) -> bool {
17        count as u64 >= self.min && self.max.is_none_or(|max| count as u64 <= max)
18    }
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum Comparison {
23    Eq,
24    Ne,
25    Lt,
26    Le,
27    Gt,
28    Ge,
29}
30impl Comparison {
31    pub fn matches(self, order: Ordering) -> bool {
32        match self {
33            Self::Eq => order == Ordering::Equal,
34            Self::Ne => order != Ordering::Equal,
35            Self::Lt => order == Ordering::Less,
36            Self::Le => order != Ordering::Greater,
37            Self::Gt => order == Ordering::Greater,
38            Self::Ge => order != Ordering::Less,
39        }
40    }
41}
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub enum AttributeValue {
44    Concepts(Box<Expr>),
45    Number(Decimal),
46    Strings(Vec<String>),
47    Boolean(bool),
48}
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct AttributeConstraint {
51    pub cardinality: Cardinality,
52    pub reverse: bool,
53    pub name: Box<Expr>,
54    pub comparison: Comparison,
55    pub value: AttributeValue,
56}
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum Refinement {
59    Attribute(AttributeConstraint),
60    Group(Cardinality, Box<Refinement>),
61    And(Vec<Refinement>),
62    Or(Vec<Refinement>),
63}
64
65impl Parser<'_> {
66    pub(super) fn keyword(&mut self, word: &str) -> bool {
67        if self.word().eq_ignore_ascii_case(word) {
68            self.pos += word.len();
69            true
70        } else {
71            false
72        }
73    }
74    /// A value keyword, which the grammar lets run straight into a following
75    /// operator: `true or` may be written `trueor`, `ANY MINUS` `ANYMINUS`, and
76    /// an attribute named `ANY NOT = x` `ANYNOT = x`.
77    pub(super) fn value_keyword(&mut self, word: &str) -> bool {
78        let found = self.word();
79        let rest = found.get(word.len()..).unwrap_or("");
80        let joined = found.len() > word.len()
81            && found[..word.len()].eq_ignore_ascii_case(word)
82            && (["and", "or", "minus"]
83                .iter()
84                .any(|op| rest.eq_ignore_ascii_case(op))
85                || rest.eq_ignore_ascii_case("not")
86                    && skip_space(&self.rest()[found.len()..]).starts_with('='));
87        if found.eq_ignore_ascii_case(word) || joined {
88            self.pos += word.len();
89            true
90        } else {
91            false
92        }
93    }
94    /// A filter keyword or member field name, lowercased. The long syntax's
95    /// `not =` may follow a keyword without a space, as in `idNOT =`, so a
96    /// known keyword ending in `not` before `=` is read without it.
97    pub(super) fn filter_name(&self) -> String {
98        const KNOWN: &[&str] = &[
99            "term",
100            "language",
101            "type",
102            "typeid",
103            "dialect",
104            "dialectid",
105            "id",
106            "moduleid",
107            "effectivetime",
108            "active",
109            "definitionstatus",
110            "definitionstatusid",
111        ];
112        let name = self.word().to_ascii_lowercase();
113        if let Some(keyword) = name.strip_suffix("not") {
114            let after = skip_space(&self.rest()[name.len()..]);
115            if KNOWN.contains(&keyword) && after.starts_with('=') {
116                return keyword.to_owned();
117            }
118        }
119        name
120    }
121    fn natural(&mut self) -> Result<(u64, std::ops::Range<usize>)> {
122        let start = self.pos;
123        while self.rest().starts_with(|c: char| c.is_ascii_digit()) {
124            self.pos += 1;
125        }
126        let text = &self.text[start..self.pos];
127        if text.is_empty() || text.len() > 1 && text.starts_with('0') {
128            return Err(self.error(ParseErrorKind::Syntax, "Leading zero in cardinality"));
129        }
130        // Every stored row/group count fits u32. Larger bounds remain above that domain.
131        Ok((text.parse().unwrap_or(u64::MAX), start..self.pos))
132    }
133    fn cardinality(&mut self) -> Result<Cardinality> {
134        if !self.take("[") {
135            return Ok(Cardinality::default());
136        }
137        let (min, min_text) = self.natural()?;
138        if !self.take("..") {
139            self.required_ws()?;
140            if !self.keyword("to") {
141                return Err(self.unexpected());
142            }
143            self.required_ws()?;
144        }
145        let max = if self.take("*") || self.keyword("many") {
146            None
147        } else {
148            Some(self.natural()?)
149        };
150        let reversed = max.as_ref().is_some_and(|(_, max_text)| {
151            let min = &self.text[min_text.clone()];
152            let max = &self.text[max_text.clone()];
153            min.len().cmp(&max.len()).then_with(|| min.cmp(max)).is_gt()
154        });
155        if !self.take("]") {
156            return Err(self.error(ParseErrorKind::Syntax, "Invalid cardinality range"));
157        }
158        if reversed {
159            // Grammatical, but no count lies between a minimum and a smaller maximum.
160            self.refuse(min_text.start, "Cardinality minimum exceeds its maximum");
161        }
162        self.ws()?;
163        Ok(Cardinality {
164            min,
165            max: max.map(|(value, _)| value),
166        })
167    }
168    pub(super) fn comparison(&mut self) -> Result<Comparison> {
169        self.ws()?;
170        for (text, op) in [
171            ("!=", Comparison::Ne),
172            ("<>", Comparison::Ne),
173            ("<=", Comparison::Le),
174            (">=", Comparison::Ge),
175            ("=", Comparison::Eq),
176            ("<", Comparison::Lt),
177            (">", Comparison::Gt),
178        ] {
179            if self.take(text) {
180                self.ws()?;
181                return Ok(op);
182            }
183        }
184        if self.keyword("not") {
185            self.ws()?;
186            if self.take("=") {
187                self.ws()?;
188                return Ok(Comparison::Ne);
189            }
190        }
191        Err(self.error(ParseErrorKind::Syntax, "Expected comparison operator"))
192    }
193    pub(super) fn quoted(&mut self) -> Result<String> {
194        if !self.take("\"") {
195            return Err(self.unexpected());
196        }
197        let mut value = String::new();
198        loop {
199            let Some(c) = self.rest().chars().next() else {
200                return Err(self.error(ParseErrorKind::Syntax, "Unclosed string"));
201            };
202            self.pos += c.len_utf8();
203            if c == '"' {
204                return Ok(value);
205            }
206            if c == '\\' {
207                let Some(escaped) = self.rest().chars().next() else {
208                    return Err(self.unexpected());
209                };
210                // The brief 2.3 grammar also permits an escaped literal asterisk.
211                if !matches!(escaped, '\\' | '"' | '*') {
212                    return Err(self.error(ParseErrorKind::Syntax, "Invalid string escape"));
213                }
214                self.pos += escaped.len_utf8();
215                value.push(escaped);
216            } else if c.is_ascii_control() && !matches!(c, '\r' | '\n' | '\t') {
217                return Err(self.unexpected());
218            } else {
219                value.push(c);
220            }
221        }
222    }
223    fn attribute(
224        &mut self,
225        depth: usize,
226        cardinality: Cardinality,
227        grouped: bool,
228    ) -> Result<Refinement> {
229        let flag = self.pos;
230        // Whitespace after a flag is optional, including before a long-form name operator.
231        let reverse = if self.starts_alternate() {
232            false
233        } else if self.text[self.pos..]
234            .get(..9)
235            .is_some_and(|prefix| prefix.eq_ignore_ascii_case("reverseof"))
236        {
237            self.pos += 9;
238            true
239        } else if self.word().eq_ignore_ascii_case("refsetcontainingany") {
240            false
241        } else {
242            self.take("R") || self.take("r")
243        };
244        if reverse && grouped {
245            // 6.2 and 6.3 define the reverse flag for whole refinements only; a reversed
246            // relationship belongs to the source concept's group, never to the tested concept's.
247            self.refuse(
248                flag,
249                "Reverse flag inside an attribute group has no defined ECL semantics",
250            );
251        }
252        self.ws()?;
253        let name = Box::new(self.subexpression(depth + 1)?);
254        let comparison = self.comparison()?;
255        let value = if self.take("#") {
256            let start = self.pos;
257            self.take("-");
258            self.take("+");
259            let integer_start = self.pos;
260            while self.rest().starts_with(|c: char| c.is_ascii_digit()) {
261                self.pos += 1;
262            }
263            if self.pos - integer_start > 1 && self.text[integer_start..].starts_with('0') {
264                return Err(self.unexpected());
265            }
266            if self.take(".") {
267                while self.rest().starts_with(|c: char| c.is_ascii_digit()) {
268                    self.pos += 1;
269                }
270            }
271            let number = Decimal::parse(&self.text[start..self.pos])
272                .ok_or_else(|| self.error(ParseErrorKind::Syntax, "Invalid decimal"))?;
273            AttributeValue::Number(number)
274        } else if self.rest().starts_with('"') && !self.starts_alternate() {
275            let value = self.quoted()?;
276            if value.is_empty() {
277                return Err(self.error(ParseErrorKind::Syntax, "Empty concrete string"));
278            }
279            AttributeValue::Strings(vec![value])
280        } else if !self.starts_alternate() && self.value_keyword("true") {
281            AttributeValue::Boolean(true)
282        } else if !self.starts_alternate() && self.value_keyword("false") {
283            AttributeValue::Boolean(false)
284        } else {
285            let saved = self.pos;
286            if self.take("(") {
287                self.ws()?;
288            }
289            if self.pos != saved && self.rest().starts_with('"') && !self.starts_alternate() {
290                let mut values = vec![self.quoted()?];
291                loop {
292                    let spaced = self.ws()?;
293                    if self.take(")") {
294                        break;
295                    }
296                    if !spaced {
297                        return Err(self.unexpected());
298                    }
299                    values.push(self.quoted()?);
300                }
301                if values.iter().any(String::is_empty) {
302                    return Err(self.unexpected());
303                }
304                AttributeValue::Strings(values)
305            } else {
306                self.pos = saved;
307                AttributeValue::Concepts(Box::new(self.subexpression(depth + 1)?))
308            }
309        };
310        if !matches!(value, AttributeValue::Number(_))
311            && !matches!(comparison, Comparison::Eq | Comparison::Ne)
312        {
313            return Err(self.error(
314                ParseErrorKind::Syntax,
315                "Ordered comparison requires a numeric attribute value",
316            ));
317        }
318        if reverse && !matches!(value, AttributeValue::Concepts(_)) {
319            // The grammar admits R with a concrete value; 6.2 defines reversal only over
320            // destination concepts, so no concrete value can be a reversed source.
321            self.refuse(
322                flag,
323                "Reverse flag with a concrete value has no defined ECL semantics",
324            );
325        }
326        Ok(Refinement::Attribute(AttributeConstraint {
327            cardinality,
328            reverse,
329            name,
330            comparison,
331            value,
332        }))
333    }
334    /// One operand of a refinement: an attribute or bracketed attribute set,
335    /// which may also join an attribute set, or else a group or a bracketed
336    /// refinement. The flag says which.
337    fn operand(&mut self, depth: usize) -> Result<(Refinement, bool)> {
338        if depth > MAX_DEPTH {
339            return Err(self.error(ParseErrorKind::Limit, "Refinement nesting exceeds 64"));
340        }
341        self.nodes += 1;
342        if self.nodes > MAX_NODES {
343            return Err(self.error(ParseErrorKind::Limit, "Too many nodes"));
344        }
345        self.ws()?;
346        let start = self.mark();
347        let cardinality = self.cardinality()?;
348        self.ws()?;
349        if self.take("{") {
350            let inner = self.attribute_set(depth + 1, true)?;
351            self.ws()?;
352            if !self.take("}") {
353                return Err(self.unexpected());
354            }
355            return Ok((Refinement::Group(cardinality, Box::new(inner)), false));
356        }
357        self.reset(start.clone());
358        let attribute = self.subattribute(depth, false);
359        if attribute.is_ok() || !self.text[start.pos..].starts_with('(') {
360            return attribute.map(|a| (a, true));
361        }
362        self.reset(start);
363        self.take("(");
364        let inner = self.refinement(depth + 1)?;
365        self.ws()?;
366        if !self.take(")") {
367            return Err(self.unexpected());
368        }
369        Ok((inner, false))
370    }
371
372    /// Attributes joined by one operator, as the grammar's `eclAttributeSet`,
373    /// which is all a group or a bracketed attribute set may hold.
374    fn attribute_set(&mut self, depth: usize, grouped: bool) -> Result<Refinement> {
375        let mut parts = vec![self.subattribute(depth, grouped)?];
376        let mut operator = None;
377        loop {
378            let before = self.mark();
379            let Some(next) = self.boolean()? else { break };
380            if next == Boolean::Minus || operator.is_some_and(|op| op != next) {
381                self.reset(before);
382                break;
383            }
384            operator = Some(next);
385            parts.push(self.subattribute(depth, grouped)?);
386        }
387        Ok(match operator {
388            None => parts.pop().expect("one part"),
389            Some(Boolean::And) => Refinement::And(parts),
390            Some(_) => Refinement::Or(parts),
391        })
392    }
393
394    /// An attribute, or a bracketed attribute set.
395    fn subattribute(&mut self, depth: usize, grouped: bool) -> Result<Refinement> {
396        if depth > MAX_DEPTH {
397            return Err(self.error(ParseErrorKind::Limit, "Refinement nesting exceeds 64"));
398        }
399        self.nodes += 1;
400        if self.nodes > MAX_NODES {
401            return Err(self.error(ParseErrorKind::Limit, "Too many nodes"));
402        }
403        self.ws()?;
404        let start = self.mark();
405        let has_cardinality = self.rest().starts_with('[');
406        let cardinality = self.cardinality()?;
407        let attribute = self.attribute(depth, cardinality, grouped);
408        if attribute.is_ok() || has_cardinality || !self.text[start.pos..].starts_with('(') {
409            return attribute;
410        }
411        self.reset(start);
412        self.take("(");
413        let inner = self.attribute_set(depth + 1, grouped)?;
414        self.ws()?;
415        if !self.take(")") {
416            return Err(self.unexpected());
417        }
418        Ok(inner)
419    }
420
421    /// Operands joined by operators.
422    ///
423    /// The grammar nests attribute sets, each with one operator, inside a
424    /// refinement with one operator, and only attributes may join a set. So an
425    /// unbracketed mix of conjunction and disjunction is grammatical exactly
426    /// when every operator beside a group or bracketed refinement agrees; it
427    /// then derives more than one way, as `a, b OR c` reads both `(a, b) OR c`
428    /// and `a, (b OR c)`. 6.4 makes brackets mandatory for such a mix, so it is
429    /// refused as ambiguous rather than read one way. Otherwise it is a syntax
430    /// error.
431    pub(super) fn refinement(&mut self, depth: usize) -> Result<Refinement> {
432        let start = self.pos;
433        let (first, attribute) = self.operand(depth)?;
434        let mut parts = vec![first];
435        let mut attributes = vec![attribute];
436        let mut operators = Vec::new();
437        while let Some(operator) = self.boolean()? {
438            if operator == Boolean::Minus {
439                return Err(self.error(
440                    ParseErrorKind::Syntax,
441                    "Exclusion is not a refinement operator",
442                ));
443            }
444            let (part, attribute) = self.operand(depth)?;
445            parts.push(part);
446            attributes.push(attribute);
447            operators.push(operator);
448        }
449        let Some(&operator) = operators.first() else {
450            return Ok(parts.pop().expect("one part"));
451        };
452        if operators.iter().any(|&op| op != operator) {
453            let mut beside_groups = operators
454                .iter()
455                .enumerate()
456                .filter(|&(i, _)| !attributes[i] || !attributes[i + 1])
457                .map(|(_, &op)| op);
458            let first = beside_groups.next();
459            if beside_groups.any(|op| Some(op) != first) {
460                return Err(self.error(
461                    ParseErrorKind::Syntax,
462                    "Mixed refinement operators require parentheses",
463                ));
464            }
465            self.refuse(
466                start,
467                "Conjunction and disjunction together require brackets (6.4)",
468            );
469        }
470        Ok(if operator == Boolean::And {
471            Refinement::And(parts)
472        } else {
473            Refinement::Or(parts)
474        })
475    }
476}
477
478/// Text after any whitespace and comments.
479fn skip_space(mut text: &str) -> &str {
480    loop {
481        text = text.trim_start_matches([' ', '\t', '\r', '\n']);
482        match text
483            .strip_prefix("/*")
484            .and_then(|c| c.find("*/").map(|end| &c[end + 2..]))
485        {
486            Some(rest) => text = rest,
487            None => return text,
488        }
489    }
490}