Skip to main content

datalogic_rs/
facts.rs

1//! Static facts about a compiled rule: which data paths it reads, which
2//! operators it uses, and whether its result is a function of its data.
3//! Backs [`crate::Logic::facts`].
4//!
5//! The walk reads the operator table for everything it needs to know about
6//! a built-in (`reads_context`, `effect`, `frames`), so a new row is
7//! classified by what it declares. The frame arithmetic is the scope pass's
8//! own ([`ScopeBinding::resolve`]), so a read is a root read here exactly
9//! when the evaluator resolves it against the root.
10
11use std::collections::BTreeSet;
12use std::fmt;
13
14use datavalue::OwnedDataValue;
15
16use crate::OpCode;
17use crate::compile::parse_path_segments;
18use crate::compile::scope::frames_pushed_for_child;
19use crate::node::{
20    CompiledMissingArg, CompiledMissingMin, CompiledMissingPaths, CompiledNode, MetadataHint,
21    PathSegment, ScopeBinding,
22};
23use crate::operators::meta::Effect;
24
25/// A path into the data a rule is evaluated against, one segment per
26/// object key or array index, from the root.
27///
28/// A read of a path observes the whole value there, everything under it
29/// included. The empty path is the whole data context.
30///
31/// Segments are kept apart rather than joined, because `var` splits its
32/// path on dots and `val` does not: `{"var": "a.b"}` reads `["a", "b"]`
33/// while `{"val": "a.b"}` reads the single key `["a.b"]`. The
34/// [`Display`](fmt::Display) form joins them with dots, so it is only
35/// unambiguous for segments that contain none.
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct DataPath {
38    segments: Vec<String>,
39}
40
41impl DataPath {
42    /// The path's segments, root first. Numeric segments are kept as
43    /// written (`"0"`): they index an array, or name a key on an object.
44    pub fn segments(&self) -> &[String] {
45        &self.segments
46    }
47
48    /// Whether this is the empty path, the whole data context.
49    pub fn is_root(&self) -> bool {
50        self.segments.is_empty()
51    }
52
53    /// Whether this path is `other` or an ancestor of it, so that a read of
54    /// this path observes `other` too.
55    pub fn covers(&self, other: &DataPath) -> bool {
56        other.segments.starts_with(&self.segments)
57    }
58
59    fn from_segments(segments: &[PathSegment]) -> Self {
60        DataPath {
61            segments: segments
62                .iter()
63                .map(|s| match s {
64                    PathSegment::Field(name) | PathSegment::FieldOrIndex(name, _) => {
65                        name.to_string()
66                    }
67                })
68                .collect(),
69        }
70    }
71}
72
73impl fmt::Display for DataPath {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str(&self.segments.join("."))
76    }
77}
78
79/// What a compiled rule reads, which operators it uses, and whether its
80/// result is a function of its data. Returned by [`crate::Logic::facts`].
81///
82/// The facts describe the **compiled** rule, after the optimizer: a branch
83/// that constant folding removed is not read, and an operator it folded
84/// away is not listed. An engine built
85/// [`with_constant_folding(false)`](crate::EngineBuilder::with_constant_folding)
86/// can therefore report more than the default engine does for the same
87/// rule. Both answers are sound for the rule they describe.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Facts {
90    reads: Vec<DataPath>,
91    computed_reads: bool,
92    operators: Vec<&'static str>,
93    custom_operators: Vec<String>,
94    deterministic: bool,
95    context_readers: bool,
96}
97
98impl Facts {
99    /// The data paths the rule reads from the root of its input, sorted,
100    /// without duplicates, and without any path another listed path covers
101    /// (reading `user` observes `user.name`).
102    ///
103    /// Only reads resolved against the root are listed. An iterator body
104    /// reads the current element, and a `try` catch arm the caught error;
105    /// those reads stay out, because the element came from the iterator's
106    /// source, which is listed. A level marker that climbs back to the
107    /// root (`{"val": [[1], "rate"]}` inside one `map`) is a root read and
108    /// is listed.
109    ///
110    /// The list is complete only when [`Self::reads_complete`] holds.
111    pub fn reads(&self) -> &[DataPath] {
112        &self.reads
113    }
114
115    /// Whether the rule reads a path that is only known at evaluation
116    /// time: a `var` / `val` path, or a `missing` / `missing_some` /
117    /// `exists` path list, computed by an expression. Such a read can
118    /// reach any part of the data, so [`Self::reads`] is then a lower
119    /// bound. The expression that computes the path is itself listed.
120    pub fn has_computed_reads(&self) -> bool {
121        self.computed_reads
122    }
123
124    /// Whether [`Self::reads`] lists everything the rule can read: no
125    /// computed path, and no custom operator that reads the context (one
126    /// can read all of it through
127    /// [`EvalContext::root_input`](crate::operator::EvalContext::root_input)).
128    /// A custom operator that does not declares so in
129    /// [`CustomOperator::info`](crate::CustomOperator::info).
130    pub fn reads_complete(&self) -> bool {
131        !self.computed_reads && !self.context_readers
132    }
133
134    /// Whether the rule can read its data at all. `false` means its result
135    /// does not depend on the data (it can still depend on the clock; see
136    /// [`Self::is_deterministic`]).
137    pub fn reads_data(&self) -> bool {
138        !self.reads_complete() || !self.reads.is_empty()
139    }
140
141    /// The built-in operators the compiled rule uses, by canonical name
142    /// (`val` for `var`, `if` for `?:`), sorted and without duplicates.
143    /// The names match [`crate::OperatorInfo::name`], so each can be looked
144    /// up in [`crate::Engine::operators`].
145    pub fn operators(&self) -> &[&'static str] {
146        &self.operators
147    }
148
149    /// The custom operators the compiled rule calls, sorted and without
150    /// duplicates.
151    pub fn custom_operators(&self) -> &[String] {
152        &self.custom_operators
153    }
154
155    /// Whether the result depends on nothing but the data: no operator
156    /// that reads the clock (`now`), and no custom operator that does not
157    /// declare itself deterministic in
158    /// [`CustomOperator::info`](crate::CustomOperator::info). Raising an
159    /// error (`throw`) is deterministic.
160    pub fn is_deterministic(&self) -> bool {
161        self.deterministic
162    }
163}
164
165#[derive(Default)]
166struct Collector {
167    reads: BTreeSet<DataPath>,
168    computed_reads: bool,
169    operators: BTreeSet<&'static str>,
170    custom_operators: BTreeSet<String>,
171    nondeterministic: bool,
172    context_readers: bool,
173}
174
175impl Collector {
176    fn read(&mut self, depth: u32, scope_level: u32, segments: &[PathSegment]) {
177        if ScopeBinding::resolve(depth, scope_level) == ScopeBinding::Root {
178            self.reads.insert(DataPath::from_segments(segments));
179        }
180    }
181
182    /// `missing` / `missing_some` look paths up in the current frame's
183    /// data, which is the root only outside every pushed frame.
184    fn missing_path(&mut self, depth: u32, segments: &[PathSegment]) {
185        self.read(depth, 0, segments);
186    }
187
188    /// A `missing` / `missing_some` argument evaluated at runtime: a
189    /// string path, or an array of them (anything else is ignored). A
190    /// literal one is as static as a pre-parsed path.
191    fn dynamic_missing_paths(&mut self, node: &CompiledNode, depth: u32) {
192        let literal = |value: &OwnedDataValue, this: &mut Self| match value {
193            OwnedDataValue::String(s) => this.missing_path(depth, &parse_path_segments(s)),
194            OwnedDataValue::Array(items) => {
195                for item in items {
196                    if let OwnedDataValue::String(s) = item {
197                        this.missing_path(depth, &parse_path_segments(s));
198                    }
199                }
200            }
201            _ => {}
202        };
203        match node {
204            CompiledNode::Value { value, .. } => literal(value, self),
205            // An unfolded literal array of literals.
206            CompiledNode::Array { nodes, .. }
207                if nodes
208                    .iter()
209                    .all(|n| matches!(n, CompiledNode::Value { .. })) =>
210            {
211                for n in nodes.iter() {
212                    if let CompiledNode::Value {
213                        value: OwnedDataValue::String(s),
214                        ..
215                    } = n
216                    {
217                        self.missing_path(depth, &parse_path_segments(s));
218                    }
219                }
220            }
221            _ => {
222                self.computed_reads = true;
223                self.walk(node, depth);
224            }
225        }
226    }
227
228    fn walk(&mut self, node: &CompiledNode, depth: u32) {
229        match node {
230            CompiledNode::Value { .. } => {}
231            CompiledNode::Var {
232                scope_level,
233                segments,
234                metadata_hint,
235                default_value,
236                ..
237            } => {
238                self.operators.insert(OpCode::Val.as_str());
239                // `index` / `key` at a metadata level read the iteration
240                // frame's metadata, not data.
241                if *metadata_hint == MetadataHint::None {
242                    self.read(depth, *scope_level, segments);
243                }
244                if let Some(default) = default_value {
245                    self.walk(default, depth);
246                }
247            }
248            #[cfg(feature = "ext-control")]
249            CompiledNode::Exists(data) => {
250                self.operators.insert(OpCode::Exists.as_str());
251                // No path names the data itself, which always exists.
252                if !data.segments.is_empty() {
253                    self.read(depth, data.scope_level, &data.segments);
254                }
255            }
256            #[cfg(feature = "error-handling")]
257            CompiledNode::Throw(_) => {
258                self.operators.insert(OpCode::Throw.as_str());
259            }
260            CompiledNode::Missing(data) => {
261                self.operators.insert(OpCode::Missing.as_str());
262                for arg in data.args.iter() {
263                    match arg {
264                        CompiledMissingArg::Now((_, segments)) => {
265                            self.missing_path(depth, segments)
266                        }
267                        CompiledMissingArg::Later(node) => self.dynamic_missing_paths(node, depth),
268                    }
269                }
270            }
271            CompiledNode::MissingSome(data) => {
272                self.operators.insert(OpCode::MissingSome.as_str());
273                if let CompiledMissingMin::Later(node) = &data.min_present {
274                    self.walk(node, depth);
275                }
276                match &data.paths {
277                    CompiledMissingPaths::Now(paths) => {
278                        for (_, segments) in paths.iter() {
279                            self.missing_path(depth, segments);
280                        }
281                    }
282                    CompiledMissingPaths::Later(node) => self.dynamic_missing_paths(node, depth),
283                }
284            }
285            CompiledNode::InvalidArgs { op_name, .. } => {
286                // Its arguments are never evaluated, so nothing is read.
287                if let Ok(op) = op_name.parse::<OpCode>() {
288                    self.operators.insert(canonical_name(op));
289                }
290            }
291            CompiledNode::CustomOperator(data) => {
292                self.custom_operators.insert(data.name.clone());
293                self.nondeterministic |= !data.info.deterministic;
294                self.context_readers |= data.info.reads_context;
295                for arg in data.args.iter() {
296                    self.walk(arg, depth);
297                }
298            }
299            CompiledNode::BuiltinOperator { opcode, args, .. } => {
300                self.builtin(*opcode, args, depth);
301            }
302            // Arrays, the CSE wrapper and output templates evaluate their
303            // children at their own depth and read nothing themselves.
304            _ => node.visit_indexed_children(&mut |_, child| self.walk(child, depth)),
305        }
306    }
307
308    fn builtin(&mut self, opcode: OpCode, args: &[CompiledNode], depth: u32) {
309        let meta = opcode.meta();
310        self.operators.insert(canonical_name(opcode));
311        if meta.effect == Effect::Clock {
312            self.nondeterministic = true;
313        }
314        if meta.reads_context {
315            match opcode {
316                // Without a bucketing expression `fractional` hashes these
317                // two fields of the root, at any depth. Which form a call
318                // takes is only known at runtime, so both are reported.
319                #[cfg(feature = "flagd")]
320                OpCode::Fractional => {
321                    for path in [&["targetingKey"][..], &["$flagd", "flagKey"]] {
322                        self.reads.insert(DataPath {
323                            segments: path.iter().map(|s| s.to_string()).collect(),
324                        });
325                    }
326                }
327                // Every other context reader that is still a generic node
328                // (a `val` / `var` / `exists` whose path or level is an
329                // expression) reads a path the compiler could not resolve.
330                _ => self.computed_reads = true,
331            }
332        }
333        let len = args.len();
334        for (index, arg) in args.iter().enumerate() {
335            self.walk(arg, depth + frames_pushed_for_child(opcode, index, len));
336        }
337    }
338
339    fn finish(self) -> Facts {
340        // A sorted set puts every path right after its ancestors, so one
341        // pass against the last kept path drops every covered one.
342        let mut reads: Vec<DataPath> = Vec::with_capacity(self.reads.len());
343        for path in self.reads {
344            if reads.last().is_none_or(|kept| !kept.covers(&path)) {
345                reads.push(path);
346            }
347        }
348        Facts {
349            reads,
350            computed_reads: self.computed_reads,
351            operators: self.operators.into_iter().collect(),
352            custom_operators: self.custom_operators.into_iter().collect(),
353            context_readers: self.context_readers,
354            deterministic: !self.nondeterministic,
355        }
356    }
357}
358
359/// The name [`crate::Engine::operators`] lists the opcode under. An
360/// internal opcode reports the operator it renders as (`VarDefault` is
361/// `val`).
362fn canonical_name(op: OpCode) -> &'static str {
363    match op.catalogue_entry().names.first() {
364        Some(name) => name,
365        None => op
366            .as_str()
367            .parse::<OpCode>()
368            .map_or(op.as_str(), OpCode::as_str),
369    }
370}
371
372pub(crate) fn collect(root: &CompiledNode) -> Facts {
373    let mut collector = Collector::default();
374    collector.walk(root, 0);
375    collector.finish()
376}