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}