Skip to main content

jsonld_core/context/
inverse.rs

1use iri_rs::IriBuf;
2
3use super::{BindingRef, Context, Key};
4use crate::{Container, Direction, HashMap, LenientLangTag, LenientLangTagBuf, Nullable, Term, Type};
5use hashbrown::hash_map::Entry;
6use std::{fmt, hash::Hash};
7
8/// Length-then-lex ordering: `true` if `a` is "shorter or, on tie, lex-less"
9/// than `b`. Matches the inverse-context build rule for picking the smaller
10/// term within a slot.
11#[inline]
12fn term_lt(a: &Key, b: &Key) -> bool {
13    let al = a.as_str().len();
14    let bl = b.as_str().len();
15    al < bl || (al == bl && a.as_str() < b.as_str())
16}
17
18#[inline]
19fn keep_smaller_opt(slot: &mut Option<Key>, candidate: &Key) {
20    match slot {
21        None => *slot = Some(*candidate),
22        Some(existing) if term_lt(candidate, existing) => *slot = Some(*candidate),
23        _ => {}
24    }
25}
26
27#[inline]
28fn keep_smaller(slot: &mut Key, candidate: &Key) {
29    if term_lt(candidate, slot) {
30        *slot = *candidate;
31    }
32}
33
34#[derive(Clone, PartialEq, Eq)]
35/// Type criterion used when selecting a term from the inverse context.
36pub enum TypeSelection<T = IriBuf> {
37    /// Select a reverse property.
38    Reverse,
39    /// Accept any type.
40    Any,
41    /// Select a term whose type mapping matches.
42    Type(Type<T>),
43}
44
45impl<T: fmt::Debug> fmt::Debug for TypeSelection<T> {
46    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
47        match self {
48            TypeSelection::Reverse => write!(f, "Reverse"),
49            TypeSelection::Any => write!(f, "Any"),
50            TypeSelection::Type(ty) => write!(f, "Type({ty:?})"),
51        }
52    }
53}
54
55struct InverseType<T> {
56    reverse: Option<Key>,
57    any: Option<Key>,
58    map: HashMap<Type<T>, Key>,
59}
60
61impl<T> InverseType<T> {
62    fn select(&self, selection: &TypeSelection<T>) -> Option<&Key>
63    where
64        T: Hash + Eq,
65    {
66        match selection {
67            TypeSelection::Reverse => self.reverse.as_ref(),
68            TypeSelection::Any => self.any.as_ref(),
69            TypeSelection::Type(ty) => self.map.get(ty),
70        }
71    }
72
73    fn set_any(&mut self, term: &Key) {
74        keep_smaller_opt(&mut self.any, term);
75    }
76
77    fn set_none(&mut self, term: &Key)
78    where
79        T: Clone + Hash + Eq,
80    {
81        self.set(&Type::None, term);
82    }
83
84    fn set(&mut self, ty: &Type<T>, term: &Key)
85    where
86        T: Clone + Hash + Eq,
87    {
88        match self.map.entry(ty.clone()) {
89            Entry::Vacant(v) => {
90                v.insert(*term);
91            }
92            Entry::Occupied(mut o) if term_lt(term, o.get()) => {
93                o.insert(*term);
94            }
95            Entry::Occupied(_) => {}
96        }
97    }
98}
99
100type LangDir = Nullable<(Option<LenientLangTagBuf>, Option<Direction>)>;
101
102/// Borrowed view over a [`LangDir`], for lookups that would otherwise have to
103/// allocate a [`LenientLangTagBuf`] just to hash it.
104///
105/// Hashes identically to the owned key: `Nullable`'s derived `Hash` walks the
106/// same fields either way, and `LenientLangTagBuf` hashes by delegating to
107/// `LenientLangTag`.
108struct LangDirRef<'a>(Nullable<(Option<&'a LenientLangTag>, Option<Direction>)>);
109
110impl Hash for LangDirRef<'_> {
111    #[inline]
112    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
113        self.0.hash(state);
114    }
115}
116
117impl hashbrown::Equivalent<LangDir> for LangDirRef<'_> {
118    #[inline]
119    fn equivalent(&self, key: &LangDir) -> bool {
120        match (&self.0, key) {
121            (Nullable::Null, Nullable::Null) => true,
122            (Nullable::Some((lang, dir)), Nullable::Some((key_lang, key_dir))) => {
123                dir == key_dir
124                    && match (lang, key_lang) {
125                        (None, None) => true,
126                        (Some(lang), Some(key_lang)) => *lang == &**key_lang,
127                        _ => false,
128                    }
129            }
130            _ => false,
131        }
132    }
133}
134
135struct InverseLang {
136    any: Option<Key>,
137    map: HashMap<LangDir, Key>,
138}
139
140#[derive(Clone, Copy, PartialEq, Eq, Debug)]
141/// Language criterion used when selecting a term from the inverse context.
142pub enum LangSelection<'a> {
143    /// Accept any language.
144    Any,
145    /// Select a term matching the given language and base direction.
146    Lang(Nullable<(Option<&'a LenientLangTag>, Option<Direction>)>),
147}
148
149impl InverseLang {
150    fn select(&self, selection: LangSelection) -> Option<&Key> {
151        match selection {
152            LangSelection::Any => self.any.as_ref(),
153            LangSelection::Lang(lang_dir) => self.map.get(&LangDirRef(lang_dir)),
154        }
155    }
156
157    fn set_any(&mut self, term: &Key) {
158        keep_smaller_opt(&mut self.any, term);
159    }
160
161    fn set_none(&mut self, term: &Key) {
162        self.set(Nullable::Some((None, None)), term);
163    }
164
165    fn set(&mut self, lang_dir: Nullable<(Option<&LenientLangTag>, Option<Direction>)>, term: &Key) {
166        let lang_dir = lang_dir.map(|(l, d)| (l.map(std::borrow::ToOwned::to_owned), d));
167        match self.map.entry(lang_dir) {
168            Entry::Vacant(v) => {
169                v.insert(*term);
170            }
171            Entry::Occupied(mut o) if term_lt(term, o.get()) => {
172                o.insert(*term);
173            }
174            Entry::Occupied(_) => {}
175        }
176    }
177}
178
179struct InverseContainer<T> {
180    language: InverseLang,
181    typ: InverseType<T>,
182    any: Any,
183}
184
185struct Any {
186    none: Key,
187}
188
189impl<T> InverseContainer<T> {
190    pub fn new(term: &Key) -> InverseContainer<T> {
191        InverseContainer {
192            language: InverseLang {
193                any: None,
194                map: HashMap::default(),
195            },
196            typ: InverseType {
197                reverse: None,
198                any: None,
199                map: HashMap::default(),
200            },
201            any: Any { none: *term },
202        }
203    }
204}
205
206/// Inverse definition of a term, indexed by container, type and language.
207pub struct InverseDefinition<T> {
208    map: HashMap<Container, InverseContainer<T>>,
209}
210
211impl<T> InverseDefinition<T> {
212    fn new() -> InverseDefinition<T> {
213        InverseDefinition { map: HashMap::default() }
214    }
215
216    fn get(&self, container: Container) -> Option<&InverseContainer<T>> {
217        self.map.get(&container)
218    }
219
220    fn reference_mut<F: FnOnce() -> InverseContainer<T>>(&mut self, container: Container, insert: F) -> &mut InverseContainer<T> {
221        self.map.entry(container).or_insert_with(insert)
222    }
223
224    /// Selects the term to compact with: containers are tried in the given
225    /// preference order, and within the first one that has entries the term
226    /// matching `selection` is returned.
227    #[must_use]
228    pub fn select(&self, containers: &[Container], selection: &Selection<T>) -> Option<&Key>
229    where
230        T: Clone + Hash + Eq,
231    {
232        for container in containers {
233            if let Some(type_lang_map) = self.get(*container) {
234                match selection {
235                    Selection::Any => return Some(&type_lang_map.any.none),
236                    Selection::Type(preferred_values) => {
237                        for item in preferred_values {
238                            if let Some(term) = type_lang_map.typ.select(item) {
239                                return Some(term);
240                            }
241                        }
242                    }
243                    Selection::Lang(preferred_values) => {
244                        for item in preferred_values {
245                            if let Some(term) = type_lang_map.language.select(*item) {
246                                return Some(term);
247                            }
248                        }
249                    }
250                }
251            }
252        }
253
254        None
255    }
256}
257
258/// Inverse context.
259pub struct InverseContext<T, B> {
260    map: HashMap<Term<T, B>, InverseDefinition<T>>,
261}
262
263/// Criterion used to pick a term out of the inverse context.
264#[derive(PartialEq)]
265pub enum Selection<'a, T> {
266    /// Accept any term.
267    Any,
268    /// Prefer terms matching one of the given type criteria, in order.
269    Type(Vec<TypeSelection<T>>),
270    /// Prefer terms matching one of the given language criteria, in order.
271    Lang(Vec<LangSelection<'a>>),
272}
273
274impl<T: fmt::Debug> fmt::Debug for Selection<'_, T> {
275    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
276        match self {
277            Selection::Any => write!(f, "Any"),
278            Selection::Type(s) => write!(f, "Type({s:?})"),
279            Selection::Lang(s) => write!(f, "Lang({s:?})"),
280        }
281    }
282}
283
284impl<T, B> InverseContext<T, B> {
285    /// Creates an empty inverse context.
286    #[must_use]
287    pub fn new() -> Self {
288        InverseContext { map: HashMap::default() }
289    }
290}
291
292impl<T: Hash + Eq, B: Hash + Eq> InverseContext<T, B> {
293    /// Checks whether the given term has an inverse definition.
294    pub fn contains(&self, term: &Term<T, B>) -> bool {
295        self.map.contains_key(term)
296    }
297
298    /// Binds the given inverse definition to `term`, replacing any previous
299    /// binding.
300    pub fn insert(&mut self, term: Term<T, B>, value: InverseDefinition<T>) {
301        self.map.insert(term, value);
302    }
303
304    /// Returns the value bound to the given key, if any.
305    pub fn get(&self, term: &Term<T, B>) -> Option<&InverseDefinition<T>> {
306        self.map.get(term)
307    }
308
309    /// Returns a mutable reference to the value bound to the given key, if any.
310    pub fn get_mut(&mut self, term: &Term<T, B>) -> Option<&mut InverseDefinition<T>> {
311        self.map.get_mut(term)
312    }
313
314    fn reference_mut<F: FnOnce() -> InverseDefinition<T>>(&mut self, term: &Term<T, B>, insert: F) -> &mut InverseDefinition<T>
315    where
316        T: Clone,
317        B: Clone,
318    {
319        self.map.entry(term.clone()).or_insert_with(insert)
320    }
321
322    /// Selects the term to compact `var` with, trying `containers` in
323    /// preference order and matching `selection` within the first container
324    /// that has entries. Returns `None` if `var` has no inverse definition
325    /// or nothing matches.
326    pub fn select(&self, var: &Term<T, B>, containers: &[Container], selection: &Selection<T>) -> Option<&Key>
327    where
328        T: Clone,
329    {
330        match self.get(var) {
331            Some(container_map) => container_map.select(containers, selection),
332            None => None,
333        }
334    }
335}
336
337impl<T, B> Default for InverseContext<T, B> {
338    fn default() -> Self {
339        Self::new()
340    }
341}
342
343impl<'a, T: Clone + Hash + Eq, B: Clone + Hash + Eq> From<&'a Context<T, B>> for InverseContext<T, B> {
344    fn from(context: &'a Context<T, B>) -> Self {
345        let mut result = InverseContext::new();
346
347        // No upfront sort: instead, every slot keeps the smaller term
348        // (length-then-lex) on collision. Same end-state as the old sort-then-
349        // first-wins approach, without the O(P log P) cost.
350        for binding in context.definitions() {
351            if let BindingRef::Normal(term, term_definition) = binding
352                && let Some(var) = term_definition.value.as_ref()
353            {
354                let container = &term_definition.container;
355                let container_map = result.reference_mut(var, InverseDefinition::new);
356                let type_lang_map = container_map.reference_mut(*container, || InverseContainer::new(term));
357
358                // `any.none` is initialized on first insert by
359                // `InverseContainer::new`; subsequent bindings still need
360                // to update it if they are smaller.
361                keep_smaller(&mut type_lang_map.any.none, term);
362
363                let type_map = &mut type_lang_map.typ;
364                let lang_map = &mut type_lang_map.language;
365
366                if term_definition.reverse_property {
367                    // If the term definition indicates that the term represents a reverse property:
368                    keep_smaller_opt(&mut type_map.reverse, term);
369                } else {
370                    match &term_definition.typ {
371                        Some(Type::None) => {
372                            // Otherwise, if term definition has a type mapping which is @none:
373                            type_map.set_any(term);
374                            lang_map.set_any(term);
375                        }
376                        Some(typ) => {
377                            // Otherwise, if term definition has a type mapping:
378                            type_map.set(typ, term);
379                        }
380                        None => {
381                            match (&term_definition.language, &term_definition.direction) {
382                                (Some(language), Some(direction)) => {
383                                    // Otherwise, if term definition has both a language mapping
384                                    // and a direction mapping:
385                                    match (language, direction) {
386                                        (Nullable::Some(language), Nullable::Some(direction)) => {
387                                            lang_map.set(Nullable::Some((Some(language.as_lenient_lang_tag_ref()), Some(*direction))), term);
388                                        }
389                                        (Nullable::Some(language), Nullable::Null) => {
390                                            lang_map.set(Nullable::Some((Some(language.as_lenient_lang_tag_ref()), None)), term);
391                                        }
392                                        (Nullable::Null, Nullable::Some(direction)) => lang_map.set(Nullable::Some((None, Some(*direction))), term),
393                                        (Nullable::Null, Nullable::Null) => lang_map.set(Nullable::Null, term),
394                                    }
395                                }
396                                (Some(language), None) => {
397                                    // Otherwise, if term definition has a language mapping (might
398                                    // be null):
399                                    match language {
400                                        Nullable::Some(language) => lang_map.set(Nullable::Some((Some(language.as_lenient_lang_tag_ref()), None)), term),
401                                        Nullable::Null => lang_map.set(Nullable::Null, term),
402                                    }
403                                }
404                                (None, Some(direction)) => {
405                                    // Otherwise, if term definition has a direction mapping (might
406                                    // be null):
407                                    match direction {
408                                        Nullable::Some(direction) => lang_map.set(Nullable::Some((None, Some(*direction))), term),
409                                        Nullable::Null => lang_map.set(Nullable::Some((None, None)), term),
410                                    }
411                                }
412                                (None, None) => {
413                                    lang_map.set(Nullable::Some((context.default_language(), context.default_base_direction())), term);
414                                    lang_map.set_none(term);
415                                    type_map.set_none(term);
416                                }
417                            }
418                        }
419                    }
420                }
421            }
422        }
423
424        result
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use crate::HashMap;
432
433    /// [`LangDirRef`] is only sound as a lookup key while it hashes exactly
434    /// like the owned [`LangDir`] it stands in for. Nothing in the type system
435    /// enforces that, and a divergence would not fail loudly — lookups would
436    /// simply stop finding their entries and compaction would quietly pick a
437    /// different term. So check it directly.
438    #[test]
439    fn lang_dir_ref_hashes_like_the_owned_key() {
440        let (owned_tag, _) = LenientLangTagBuf::new("en-GB".to_string());
441        let (borrowed_tag, _) = LenientLangTag::new("en-GB");
442
443        let cases: Vec<(LangDir, LangDirRef)> = vec![
444            (Nullable::Null, LangDirRef(Nullable::Null)),
445            (Nullable::Some((None, None)), LangDirRef(Nullable::Some((None, None)))),
446            (
447                Nullable::Some((None, Some(Direction::Rtl))),
448                LangDirRef(Nullable::Some((None, Some(Direction::Rtl)))),
449            ),
450            (
451                Nullable::Some((Some(owned_tag.clone()), None)),
452                LangDirRef(Nullable::Some((Some(borrowed_tag), None))),
453            ),
454            (
455                Nullable::Some((Some(owned_tag), Some(Direction::Ltr))),
456                LangDirRef(Nullable::Some((Some(borrowed_tag), Some(Direction::Ltr)))),
457            ),
458        ];
459
460        for (owned, borrowed) in cases {
461            let mut map: HashMap<LangDir, u32> = HashMap::default();
462            map.insert(owned.clone(), 42);
463            assert_eq!(map.get(&borrowed), Some(&42), "borrowed lookup missed {owned:?}");
464        }
465    }
466
467    /// A key that differs must still miss — the check above would also pass if
468    /// `equivalent` said yes to everything.
469    #[test]
470    fn lang_dir_ref_still_distinguishes_keys() {
471        let (owned_tag, _) = LenientLangTagBuf::new("en".to_string());
472        let (other_tag, _) = LenientLangTag::new("fr");
473
474        let mut map: HashMap<LangDir, u32> = HashMap::default();
475        map.insert(Nullable::Some((Some(owned_tag), None)), 42);
476
477        assert_eq!(map.get(&LangDirRef(Nullable::Some((Some(other_tag), None)))), None);
478        assert_eq!(map.get(&LangDirRef(Nullable::Null)), None);
479    }
480}