Skip to main content

snomed_ecl_engine/
eval.rs

1//! Sorted ordinal sets with bounded evaluation work and temporary storage.
2use crate::ecl::{Expr, Hierarchy, MAX_DEPTH, MAX_NODES};
3use crate::store::NumericStore;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::{error::Error, fmt};
6mod descriptions;
7mod filters;
8mod history;
9mod members;
10mod membership;
11pub use members::QueryResult;
12mod refinement;
13mod values;
14
15#[derive(Clone, Copy)]
16pub struct Limits {
17    pub max_work: u64,
18    pub max_live_set_values: usize,
19}
20impl Default for Limits {
21    fn default() -> Self {
22        Self {
23            max_work: 100_000_000,
24            max_live_set_values: 8_000_000,
25        }
26    }
27}
28#[derive(Debug, PartialEq, Eq)]
29pub enum EvalError {
30    WorkLimit,
31    MemoryLimit,
32    Cancelled,
33    InvalidAst,
34    /// The engine does not implement this valid ECL form yet.
35    Unsupported(&'static str),
36    /// The requested combination has no supported semantic interpretation.
37    Semantic(String),
38    Index(String),
39    Text(String),
40    InvalidField(String),
41    TypeMismatch,
42    /// A projected concept identifier names no concept of this substrate.
43    MissingReference(String),
44    UnconfiguredAlias(String),
45}
46impl fmt::Display for EvalError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "Evaluation failed: {self:?}")
49    }
50}
51impl Error for EvalError {}
52type Result<T> = std::result::Result<T, EvalError>;
53
54pub fn evaluate(store: &NumericStore, expression: &Expr) -> Result<Vec<u32>> {
55    evaluate_with_limits(store, expression, Limits::default(), None)
56}
57pub fn evaluate_result(store: &NumericStore, expression: &Expr) -> Result<QueryResult> {
58    evaluate_result_with_limits(store, expression, Limits::default(), None)
59}
60pub fn evaluate_result_with_limits(
61    store: &NumericStore,
62    expression: &Expr,
63    limits: Limits,
64    cancelled: Option<&AtomicBool>,
65) -> Result<QueryResult> {
66    let mut context = Context {
67        store,
68        limits,
69        cancelled,
70        work: 0,
71        live: 0,
72        nodes: 0,
73        marks: Marks::borrow(),
74    };
75    context.result(expression, 0, true)
76}
77pub fn evaluate_with_limits(
78    store: &NumericStore,
79    expression: &Expr,
80    limits: Limits,
81    cancelled: Option<&AtomicBool>,
82) -> Result<Vec<u32>> {
83    Context {
84        store,
85        limits,
86        cancelled,
87        work: 0,
88        live: 0,
89        nodes: 0,
90        marks: Marks::borrow(),
91    }
92    .eval(expression, 0)
93}
94/// Past this many concepts a descendant focus is not materialised when the
95/// refinement names fewer candidates than the edition's sixteenth.
96const SMALL_FOCUS: usize = 4096;
97
98/// The focus of a refinement, as far as it has been evaluated.
99enum Focus {
100    Everything,
101    Set(Vec<u32>),
102    /// A descendant or child operator whose answer exceeds `SMALL_FOCUS`.
103    Below(Hierarchy, Vec<u32>),
104}
105
106struct Context<'a> {
107    store: &'a NumericStore,
108    limits: Limits,
109    cancelled: Option<&'a AtomicBool>,
110    work: u64,
111    live: usize,
112    nodes: usize,
113    marks: Marks,
114}
115
116/// Visit markers shared by every hierarchy operator.
117///
118/// A marker is set when it holds the current stamp, so starting a traversal is
119/// an increment rather than clearing an array the size of the edition. Stamps
120/// are single bytes, which keeps a full traversal's memory traffic the same as
121/// a plain boolean array; after 255 traversals both arrays are cleared once,
122/// which costs about a millisecond and happens rarely.
123///
124/// The arrays outlive the query: each thread keeps its own, so a process that
125/// answers many questions allocates them once rather than page-faulting a
126/// fresh pair in for every expression.
127#[derive(Default)]
128struct Marks {
129    seen: Vec<u8>,
130    selected: Vec<u8>,
131    stamp: u8,
132}
133impl Marks {
134    fn next(&mut self, n: usize) -> u8 {
135        if self.seen.len() != n {
136            self.seen = vec![0; n];
137            self.selected = vec![0; n];
138            self.stamp = 0;
139        }
140        if self.stamp == u8::MAX {
141            self.seen.fill(0);
142            self.selected.fill(0);
143            self.stamp = 0;
144        }
145        self.stamp += 1;
146        self.stamp
147    }
148    /// This thread's markers, left behind by its previous query if any.
149    fn borrow() -> Self {
150        MARKS.with(|slot| std::mem::take(&mut *slot.borrow_mut()))
151    }
152}
153
154thread_local! {
155    static MARKS: std::cell::RefCell<Marks> = std::cell::RefCell::new(Marks::default());
156}
157
158impl Drop for Context<'_> {
159    fn drop(&mut self) {
160        let marks = std::mem::take(&mut self.marks);
161        MARKS.with(|slot| *slot.borrow_mut() = marks);
162    }
163}
164
165impl Context<'_> {
166    fn tick(&mut self, work: usize) -> Result<()> {
167        if self
168            .cancelled
169            .is_some_and(|flag| flag.load(Ordering::Relaxed))
170        {
171            return Err(EvalError::Cancelled);
172        }
173        self.work = self
174            .work
175            .checked_add(work as u64)
176            .ok_or(EvalError::WorkLimit)?;
177        if self.work > self.limits.max_work {
178            return Err(EvalError::WorkLimit);
179        }
180        Ok(())
181    }
182    fn reserve(&mut self, values: usize) -> Result<Vec<u32>> {
183        self.claim(values)?;
184        Ok(Vec::with_capacity(values))
185    }
186    fn claim(&mut self, values: usize) -> Result<()> {
187        self.live = self
188            .live
189            .checked_add(values)
190            .ok_or(EvalError::MemoryLimit)?;
191        if self.live > self.limits.max_live_set_values {
192            return Err(EvalError::MemoryLimit);
193        }
194        Ok(())
195    }
196    fn release(&mut self, values: Vec<u32>) {
197        self.live -= values.capacity();
198    }
199    fn eval(&mut self, expr: &Expr, depth: usize) -> Result<Vec<u32>> {
200        let result = self.result(expr, depth, false)?;
201        self.concept_values(result)
202    }
203    fn eval_concepts(&mut self, expr: &Expr, depth: usize) -> Result<Vec<u32>> {
204        self.tick(1)?;
205        self.nodes += 1;
206        if depth > MAX_DEPTH * 3 || self.nodes > MAX_NODES {
207            return Err(EvalError::InvalidAst);
208        }
209        match expr {
210            Expr::AlternateIdentifier { scheme, code } => {
211                let id = self
212                    .store
213                    .config
214                    .identifier_schemes
215                    .get(&scheme.to_ascii_lowercase())
216                    .ok_or_else(|| EvalError::UnconfiguredAlias(scheme.clone()))?;
217                let index = self
218                    .store
219                    .identifiers
220                    .get()
221                    .map_err(|e| EvalError::Index(e.to_string()))?
222                    .ok_or(EvalError::Unsupported(
223                        "Identifier index is absent; rebuild from RF2",
224                    ))?;
225                self.tick(index.rows.len().checked_ilog2().unwrap_or(0) as usize + code.len() + 1)?;
226                let mut result = self.reserve(1)?;
227                if let Some(code) = index.lookup(*id, code) {
228                    let ordinal = self.store.ordinal(code).ok_or_else(|| {
229                        EvalError::Index("Identifier refers to an absent concept".into())
230                    })?;
231                    result.push(ordinal);
232                }
233                Ok(result)
234            }
235            Expr::DialectAlias(alias) => {
236                let id = self
237                    .store
238                    .config
239                    .dialects
240                    .get(&alias.to_ascii_lowercase())
241                    .ok_or_else(|| EvalError::UnconfiguredAlias(alias.clone()))?;
242                let mut result = self.reserve(1)?;
243                if let Some(ordinal) = self.store.ordinal(*id) {
244                    result.push(ordinal);
245                }
246                Ok(result)
247            }
248            Expr::History(inner, supplement) => self.history(inner, supplement, depth + 1),
249            Expr::DescriptionFiltered(inner, filters) => {
250                let candidates = self.eval(inner, depth + 1)?;
251                self.description_filters(candidates, filters, depth + 1)
252            }
253            Expr::ConceptFiltered(inner, filters) => {
254                let candidates = self.eval(inner, depth + 1)?;
255                self.concept_filters(candidates, filters, depth + 1)
256            }
257            Expr::MemberOf(inner) | Expr::RefsetContainingAny(inner) => {
258                let candidates = self.eval(inner, depth + 1)?;
259                let result =
260                    self.membership(&candidates, matches!(expr, Expr::RefsetContainingAny(_)))?;
261                self.release(candidates);
262                Ok(result)
263            }
264            Expr::Refined(focus, refinement) => {
265                let prepared = self.prepare(refinement, depth + 1, false)?;
266                // Test only concepts that can satisfy the refinement, when it
267                // names them. `*` then needn't be materialised at all, nor a
268                // large descendant focus: the few candidates are checked
269                // against its seeds by walking up instead.
270                let focus = match focus.as_ref() {
271                    Expr::All => Focus::Everything,
272                    Expr::Hierarchy(op, inner) if !op.ancestors() => {
273                        let seeds = self.eval(inner, depth + 1)?;
274                        match self.hierarchy_within(*op, &seeds, SMALL_FOCUS)? {
275                            Some(set) => {
276                                self.release(seeds);
277                                Focus::Set(set)
278                            }
279                            None => Focus::Below(*op, seeds),
280                        }
281                    }
282                    other => Focus::Set(self.eval(other, depth + 1)?),
283                };
284                let limit = match &focus {
285                    Focus::Set(set) => set.len(),
286                    _ => self.store.ids.len(),
287                };
288                let n = self.store.ids.len();
289                let candidates = match (self.candidates(&prepared, limit)?, focus) {
290                    (Some(bound), Focus::Everything) => {
291                        self.tick(bound.len())?;
292                        self.claim(bound.capacity())?;
293                        bound
294                    }
295                    (Some(bound), Focus::Below(op, seeds)) if bound.len() <= n / 16 => {
296                        let tested = self.below(op, &seeds, &bound)?;
297                        self.release(seeds);
298                        tested
299                    }
300                    (bound, Focus::Below(op, seeds)) => {
301                        let set = self.hierarchy(op, &seeds)?;
302                        self.release(seeds);
303                        match bound {
304                            Some(bound) => self.within(set, &bound)?,
305                            None => set,
306                        }
307                    }
308                    (Some(bound), Focus::Set(set)) => self.within(set, &bound)?,
309                    (None, Focus::Set(set)) => set,
310                    (None, Focus::Everything) => self.eval(&Expr::All, depth + 1)?,
311                };
312                let mut result = self.reserve(candidates.len())?;
313                for &source in &candidates {
314                    if self.matches_refinement(&prepared, source, None)? {
315                        result.push(source);
316                    }
317                }
318                self.release(candidates);
319                self.release_prepared(prepared);
320                Ok(result)
321            }
322            Expr::Extremum { top: true, inner } => {
323                let candidates = self.eval(inner, depth + 1)?;
324                let result = self.top(&candidates)?;
325                self.release(candidates);
326                Ok(result)
327            }
328            Expr::Extremum { top, inner } => {
329                let candidates = self.eval(inner, depth + 1)?;
330                let excluded = self.hierarchy(
331                    if *top {
332                        Hierarchy::Descendant
333                    } else {
334                        Hierarchy::Ancestor
335                    },
336                    &candidates,
337                )?;
338                let result = self.merge(&candidates, &excluded, 2)?;
339                self.release(candidates);
340                self.release(excluded);
341                Ok(result)
342            }
343            Expr::Concept(code) => {
344                let ordinal = self.store.ordinal(*code);
345                let mut result = self.reserve(usize::from(ordinal.is_some()))?;
346                result.extend(ordinal);
347                Ok(result)
348            }
349            Expr::All => {
350                self.tick(self.store.ids.len())?;
351                let count = self.store.ids.len();
352                let mut result = self.reserve(count)?;
353                result.extend(0..count as u32);
354                Ok(result)
355            }
356            Expr::Hierarchy(op, inner) => {
357                let seeds = self.eval(inner, depth + 1)?;
358                let result = self.hierarchy(*op, &seeds)?;
359                self.release(seeds);
360                Ok(result)
361            }
362            Expr::Members(_)
363            | Expr::Dotted(_, _)
364            | Expr::And(_)
365            | Expr::Or(_)
366            | Expr::Minus(_, _) => Err(EvalError::InvalidAst),
367        }
368    }
369    /// Walks the hierarchy from `seeds`, paying only for what it touches.
370    ///
371    /// The earlier version charged the size of the whole edition twice per
372    /// operator and scanned every concept to collect its answer, so `<< X`
373    /// cost the same whether X had three descendants or a hundred thousand,
374    /// and one expression could hold only about forty subsumptions before the
375    /// work budget ran out. Here the work is the edges walked, and the answer
376    /// is collected as it is found and sorted, so a union of hundreds of small
377    /// hierarchies costs what those hierarchies cost.
378    fn hierarchy(&mut self, op: Hierarchy, seeds: &[u32]) -> Result<Vec<u32>> {
379        Ok(self.hierarchy_within(op, seeds, usize::MAX)?.unwrap())
380    }
381    /// The hierarchy of `seeds`, or `None` once it exceeds `cap` concepts.
382    fn hierarchy_within(
383        &mut self,
384        op: Hierarchy,
385        seeds: &[u32],
386        cap: usize,
387    ) -> Result<Option<Vec<u32>>> {
388        if seeds.is_empty() {
389            return self.reserve(0).map(Some);
390        }
391        let store = self.store;
392        let graph = if op.ancestors() {
393            &store.parents
394        } else {
395            &store.children
396        };
397        let n = store.ids.len();
398        let stamp = self.marks.next(n);
399        self.tick(seeds.len())?;
400        // Past this many results, reading the markers back in order beats
401        // sorting a list, so the list stops growing and the markers are used.
402        let large = n / 16;
403        let mut count = 0usize;
404        let mut stack = Vec::with_capacity(seeds.len());
405        let mut found = Vec::new();
406        for &seed in seeds {
407            let i = seed as usize;
408            if self.marks.seen[i] != stamp {
409                self.marks.seen[i] = stamp;
410                stack.push(seed);
411            }
412            if op.include_self() && self.marks.selected[i] != stamp {
413                self.marks.selected[i] = stamp;
414                count += 1;
415                if count <= large {
416                    found.push(seed);
417                }
418            }
419        }
420        while let Some(node) = stack.pop() {
421            let next = graph.get(node);
422            self.tick(1 + next.len())?;
423            for &concept in next {
424                let i = concept as usize;
425                if self.marks.selected[i] != stamp {
426                    self.marks.selected[i] = stamp;
427                    count += 1;
428                    if count <= large {
429                        found.push(concept);
430                    }
431                    if count > cap {
432                        return Ok(None);
433                    }
434                }
435                if !op.direct() && self.marks.seen[i] != stamp {
436                    self.marks.seen[i] = stamp;
437                    stack.push(concept);
438                }
439            }
440        }
441        // Every set operation downstream merges sorted lists. A small answer is
442        // sorted, at k log k; a large one is read back from the markers in
443        // order, at one sequential pass over the edition.
444        if count > large {
445            self.tick(n)?;
446            found = Vec::with_capacity(count);
447            found.extend(
448                self.marks
449                    .selected
450                    .iter()
451                    .enumerate()
452                    .filter(|&(_, &mark)| mark == stamp)
453                    .map(|(i, _)| i as u32),
454            );
455        } else {
456            self.tick(found.len())?;
457            found.sort_unstable();
458        }
459        found.shrink_to_fit();
460        self.claim(found.capacity())?;
461        Ok(Some(found))
462    }
463    /// The members of `set` with no proper ancestor in it.
464    ///
465    /// Walks upward, so the cost is the set's ancestors rather than its
466    /// descendants, which for a broad concept are much of the edition.
467    fn top(&mut self, set: &[u32]) -> Result<Vec<u32>> {
468        let stamp = self.mark_seeds(set)?;
469        let mut result = self.reserve(set.len())?;
470        let mut stack = Vec::new();
471        for &member in set {
472            if !self.has_seed_above(member, stamp, &mut stack)? {
473                result.push(member);
474            }
475        }
476        Ok(result)
477    }
478
479    /// The members of `candidates` in `op` of `seeds`, for a descendant or
480    /// child operator, found by walking up from each candidate rather than
481    /// down from the seeds. Cheaper when the candidates are few and the
482    /// seeds' descendants many.
483    fn below(&mut self, op: Hierarchy, seeds: &[u32], candidates: &[u32]) -> Result<Vec<u32>> {
484        debug_assert!(!op.ancestors());
485        let stamp = self.mark_seeds(seeds)?;
486        let mut result = self.reserve(candidates.len())?;
487        let mut stack = Vec::new();
488        for &candidate in candidates {
489            let is_seed = seeds.binary_search(&candidate).is_ok();
490            self.tick(1)?;
491            let inside = if op.include_self() && is_seed {
492                true
493            } else if op.direct() {
494                let parents = self.store.parents.get(candidate);
495                self.tick(parents.len())?;
496                parents.iter().any(|p| seeds.binary_search(p).is_ok())
497            } else {
498                self.has_seed_above(candidate, stamp, &mut stack)?
499            };
500            if inside {
501                result.push(candidate);
502            }
503        }
504        Ok(result)
505    }
506
507    /// Starts a memoised upward search: `seen` means the answer for a concept
508    /// is known, `selected` that the concept is a seed or lies below one.
509    fn mark_seeds(&mut self, seeds: &[u32]) -> Result<u8> {
510        let stamp = self.marks.next(self.store.ids.len());
511        self.tick(seeds.len())?;
512        for &seed in seeds {
513            self.marks.seen[seed as usize] = stamp;
514            self.marks.selected[seed as usize] = stamp;
515        }
516        Ok(stamp)
517    }
518
519    /// Whether a proper ancestor of `concept` is a seed. Each concept's answer
520    /// is kept, so a search costs the ancestors not already resolved.
521    fn has_seed_above(
522        &mut self,
523        concept: u32,
524        stamp: u8,
525        stack: &mut Vec<(u32, usize)>,
526    ) -> Result<bool> {
527        let store = self.store;
528        let parents = store.parents.get(concept);
529        self.tick(1 + parents.len())?;
530        for &parent in parents {
531            if self.marks.seen[parent as usize] != stamp {
532                self.marks.seen[parent as usize] = stamp;
533                stack.push((parent, 0));
534                while let Some((node, next)) = stack.last_mut() {
535                    let Some(&up) = store.parents.get(*node).get(*next) else {
536                        // Nothing above reaches a seed.
537                        stack.pop();
538                        continue;
539                    };
540                    *next += 1;
541                    self.tick(1)?;
542                    if self.marks.seen[up as usize] != stamp {
543                        self.marks.seen[up as usize] = stamp;
544                        stack.push((up, 0));
545                    } else if self.marks.selected[up as usize] == stamp {
546                        // Every concept on the stack lies below `up`.
547                        for (node, _) in stack.drain(..) {
548                            self.marks.selected[node as usize] = stamp;
549                        }
550                    }
551                }
552            }
553            if self.marks.selected[parent as usize] == stamp {
554                return Ok(true);
555            }
556        }
557        Ok(false)
558    }
559    /// `set` restricted to the sorted `bound`, releasing `set`.
560    fn within(&mut self, set: Vec<u32>, bound: &[u32]) -> Result<Vec<u32>> {
561        self.tick(set.len().min(bound.len()) + bound.len())?;
562        let tested = refinement::intersect(&set, bound);
563        self.release(set);
564        self.claim(tested.capacity())?;
565        Ok(tested)
566    }
567    fn merge(&mut self, left: &[u32], right: &[u32], mode: u8) -> Result<Vec<u32>> {
568        self.tick(left.len() + right.len())?;
569        let capacity = match mode {
570            0 => left.len().min(right.len()),
571            1 => (left.len() + right.len()).min(self.store.ids.len()),
572            _ => left.len(),
573        };
574        let mut result = self.reserve(capacity)?;
575        let (mut a, mut b) = (0, 0);
576        while a < left.len() || b < right.len() {
577            if b == right.len() || a < left.len() && left[a] < right[b] {
578                if mode != 0 {
579                    result.push(left[a]);
580                }
581                a += 1;
582            } else if a == left.len() || right[b] < left[a] {
583                if mode == 1 {
584                    result.push(right[b]);
585                }
586                b += 1;
587            } else {
588                if mode != 2 {
589                    result.push(left[a]);
590                }
591                a += 1;
592                b += 1;
593            }
594        }
595        Ok(result)
596    }
597}