Skip to main content

datalog_structural_model/ir/
symbols.rs

1//! Predicate, variable, and constant symbols used by the
2//! Datalog IR.
3//!
4//! All three carry their textual identity as `Arc<str>` so the
5//! evaluator hot-path binding clones and trail rollback
6//! becomes a refcount bump instead of a `String::clone` →
7//! memmove. The wire / debug surface is unchanged; constructors
8//! still accept `impl Into<String>` so callers don't see the
9//! switch.
10//!
11//! [`VariableSymbol`] additionally caches a precomputed `FxHash`
12//! of its name so `HashMap` lookups on the forward engine's
13//! `Binding` skip rehashing the name string every time. The
14//! cached value is `state.write_u64`-fed into whatever hasher
15//! the map uses (Fx today, deterministic per-name regardless).
16//! Trail/profile justification: samply showed
17//! `Binding::get` (`FxHash` + memcmp) costing ~15 % of total
18//! samples on a rule-materialization benchmark after replacing
19//! `SipHash` with `FxHashMap`.
20
21use std::hash::{Hash, Hasher};
22use std::sync::Arc;
23
24use rustc_hash::FxHasher;
25
26/// Predicate symbol in a Datalog atom.
27#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
28pub struct PredicateSymbol(Arc<str>);
29
30impl PredicateSymbol {
31    /// Construct a predicate symbol.
32    #[must_use]
33    pub fn new(value: impl Into<String>) -> Self {
34        Self(Arc::from(value.into()))
35    }
36
37    /// Access the raw predicate identifier.
38    #[must_use]
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42}
43
44/// Variable symbol used in Datalog terms.
45///
46/// Carries a precomputed `FxHash` of its name alongside the
47/// `Arc<str>` so [`std::collections::HashMap`]-keyed lookups (notably the forward
48/// engine bindings) hash a single `u64` instead of walking
49/// the name bytes on every probe.
50#[derive(Clone, Debug)]
51pub struct VariableSymbol {
52    name: Arc<str>,
53    cached_hash: u64,
54}
55
56impl VariableSymbol {
57    /// Construct a variable symbol. Computes the cached hash of
58    /// `name` once; subsequent `Hash` calls feed that `u64` to
59    /// whatever hasher the map uses.
60    #[must_use]
61    pub fn new(value: impl Into<String>) -> Self {
62        let name: Arc<str> = Arc::from(value.into());
63        let cached_hash = {
64            let mut h = FxHasher::default();
65            h.write(name.as_bytes());
66            h.finish()
67        };
68        Self { name, cached_hash }
69    }
70
71    /// Access the raw variable identifier.
72    #[must_use]
73    pub fn as_str(&self) -> &str {
74        &self.name
75    }
76}
77
78impl PartialEq for VariableSymbol {
79    fn eq(&self, other: &Self) -> bool {
80        // Hash inequality is a fast 8-byte prefilter that lets
81        // hash hits short-circuit before walking the full name.
82        // True equality still requires identical names because
83        // the cached hash is derived from them; the inequality
84        // case is the common one on HashMap probes.
85        self.cached_hash == other.cached_hash && self.name == other.name
86    }
87}
88
89impl Eq for VariableSymbol {}
90
91impl Hash for VariableSymbol {
92    fn hash<H: Hasher>(&self, state: &mut H) {
93        state.write_u64(self.cached_hash);
94    }
95}
96
97impl PartialOrd for VariableSymbol {
98    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
99        Some(self.cmp(other))
100    }
101}
102
103impl Ord for VariableSymbol {
104    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
105        // Stable, content-addressed ordering — compare names, not
106        // hashes, so `BTreeSet` / `BTreeMap` order is independent
107        // of the hasher seed.
108        self.name.cmp(&other.name)
109    }
110}
111
112/// Constant symbol used in Datalog terms.
113///
114/// Like [`VariableSymbol`], carries a precomputed `FxHash` of its
115/// value alongside the `Arc<str>`. Constants often dominate
116/// evaluator hot paths — a hash-join frontier index
117/// (`FxHashMap<GroundTerm, _>`) hashes a constant on every probe,
118/// and `unify_one` compares constants on every fact cell — so a
119/// cached hash (fed as a single `u64`) plus a hash-prefiltered
120/// equality cut the per-fact term cost. Profile justification:
121/// profiling of a bulk rule-materialization benchmark showed
122/// `GroundTerm` `PartialEq` plus hash-join
123/// index's `GroundTerm` hashing at ~20 % of total samples, all
124/// content-based on the constant `Arc<str>`.
125///
126/// Ordering stays content-based so `BTreeSet` / `BTreeMap` order
127/// (a relation prefix index may rely on it for range queries) is
128/// independent of the hasher seed — the same split
129/// [`VariableSymbol`] makes.
130#[derive(Clone, Debug)]
131pub struct ConstantSymbol {
132    value: Arc<str>,
133    cached_hash: u64,
134}
135
136impl ConstantSymbol {
137    /// Construct a constant symbol. Computes the cached hash of
138    /// `value` once; subsequent `Hash` calls feed that `u64` to
139    /// whatever hasher the map uses.
140    #[must_use]
141    pub fn new(value: impl Into<String>) -> Self {
142        let value: Arc<str> = Arc::from(value.into());
143        let cached_hash = {
144            let mut h = FxHasher::default();
145            h.write(value.as_bytes());
146            h.finish()
147        };
148        Self { value, cached_hash }
149    }
150
151    /// Access the raw constant identifier.
152    #[must_use]
153    pub fn as_str(&self) -> &str {
154        &self.value
155    }
156}
157
158impl PartialEq for ConstantSymbol {
159    fn eq(&self, other: &Self) -> bool {
160        // Hash inequality is a fast 8-byte prefilter; true equality
161        // still requires identical values (the cached hash is
162        // derived from them). Inequality is the common case on the
163        // hash-join probe and on `unify_one`'s constant comparisons.
164        self.cached_hash == other.cached_hash && self.value == other.value
165    }
166}
167
168impl Eq for ConstantSymbol {}
169
170impl Hash for ConstantSymbol {
171    fn hash<H: Hasher>(&self, state: &mut H) {
172        state.write_u64(self.cached_hash);
173    }
174}
175
176impl PartialOrd for ConstantSymbol {
177    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
178        Some(self.cmp(other))
179    }
180}
181
182impl Ord for ConstantSymbol {
183    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
184        // Content-addressed ordering — compare values, not hashes,
185        // so `BTreeSet` / `BTreeMap` order is deterministic and
186        // hasher-independent.
187        self.value.cmp(&other.value)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use std::collections::{BTreeSet, HashMap};
194
195    use super::*;
196
197    /// The hand-written `Eq`/`Hash` must keep the std invariant
198    /// `a == b ⟹ hash(a) == hash(b)`: equal values resolve to the
199    /// same `HashMap` slot regardless of which `Arc` allocation they
200    /// came from.
201    #[test]
202    fn equal_constants_hash_and_compare_equal() {
203        let a = ConstantSymbol::new("http://ex.org/x");
204        let b = ConstantSymbol::new(String::from("http://ex.org/x"));
205        assert_eq!(a, b);
206
207        let mut map: HashMap<ConstantSymbol, u32> = HashMap::new();
208        map.insert(a, 7);
209        assert_eq!(map.get(&b), Some(&7), "equal values must hash to one slot");
210    }
211
212    /// Distinct values stay distinct (the cached-hash prefilter is an
213    /// optimisation, not a substitute for value equality).
214    #[test]
215    fn distinct_constants_are_unequal() {
216        let a = ConstantSymbol::new("http://ex.org/x");
217        let b = ConstantSymbol::new("http://ex.org/y");
218        assert_ne!(a, b);
219    }
220
221    /// Ordering stays content-based (lexicographic on the value), so
222    /// `BTreeSet`/`BTreeMap` order is independent of the cached hash —
223    /// the property the `FactSet` prefix-range index relies on.
224    #[test]
225    fn ordering_is_content_based() {
226        let set: BTreeSet<ConstantSymbol> = ["c", "a", "b"]
227            .into_iter()
228            .map(ConstantSymbol::new)
229            .collect();
230        let ordered: Vec<&str> = set.iter().map(ConstantSymbol::as_str).collect();
231        assert_eq!(ordered, ["a", "b", "c"]);
232    }
233}