use std::collections::BTreeSet;
use std::fmt;
use datavalue::OwnedDataValue;
use crate::OpCode;
use crate::compile::parse_path_segments;
use crate::compile::scope::frames_pushed_for_child;
use crate::node::{
CompiledMissingArg, CompiledMissingMin, CompiledMissingPaths, CompiledNode, MetadataHint,
PathSegment, ScopeBinding,
};
use crate::operators::meta::Effect;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DataPath {
segments: Vec<String>,
}
impl DataPath {
pub fn segments(&self) -> &[String] {
&self.segments
}
pub fn is_root(&self) -> bool {
self.segments.is_empty()
}
pub fn covers(&self, other: &DataPath) -> bool {
other.segments.starts_with(&self.segments)
}
fn from_segments(segments: &[PathSegment]) -> Self {
DataPath {
segments: segments
.iter()
.map(|s| match s {
PathSegment::Field(name) | PathSegment::FieldOrIndex(name, _) => {
name.to_string()
}
})
.collect(),
}
}
}
impl fmt::Display for DataPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.segments.join("."))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Facts {
reads: Vec<DataPath>,
computed_reads: bool,
operators: Vec<&'static str>,
custom_operators: Vec<String>,
deterministic: bool,
context_readers: bool,
}
impl Facts {
pub fn reads(&self) -> &[DataPath] {
&self.reads
}
pub fn has_computed_reads(&self) -> bool {
self.computed_reads
}
pub fn reads_complete(&self) -> bool {
!self.computed_reads && !self.context_readers
}
pub fn reads_data(&self) -> bool {
!self.reads_complete() || !self.reads.is_empty()
}
pub fn operators(&self) -> &[&'static str] {
&self.operators
}
pub fn custom_operators(&self) -> &[String] {
&self.custom_operators
}
pub fn is_deterministic(&self) -> bool {
self.deterministic
}
}
#[derive(Default)]
struct Collector {
reads: BTreeSet<DataPath>,
computed_reads: bool,
operators: BTreeSet<&'static str>,
custom_operators: BTreeSet<String>,
nondeterministic: bool,
context_readers: bool,
}
impl Collector {
fn read(&mut self, depth: u32, scope_level: u32, segments: &[PathSegment]) {
if ScopeBinding::resolve(depth, scope_level) == ScopeBinding::Root {
self.reads.insert(DataPath::from_segments(segments));
}
}
fn missing_path(&mut self, depth: u32, segments: &[PathSegment]) {
self.read(depth, 0, segments);
}
fn dynamic_missing_paths(&mut self, node: &CompiledNode, depth: u32) {
let literal = |value: &OwnedDataValue, this: &mut Self| match value {
OwnedDataValue::String(s) => this.missing_path(depth, &parse_path_segments(s)),
OwnedDataValue::Array(items) => {
for item in items {
if let OwnedDataValue::String(s) = item {
this.missing_path(depth, &parse_path_segments(s));
}
}
}
_ => {}
};
match node {
CompiledNode::Value { value, .. } => literal(value, self),
CompiledNode::Array { nodes, .. }
if nodes
.iter()
.all(|n| matches!(n, CompiledNode::Value { .. })) =>
{
for n in nodes.iter() {
if let CompiledNode::Value {
value: OwnedDataValue::String(s),
..
} = n
{
self.missing_path(depth, &parse_path_segments(s));
}
}
}
_ => {
self.computed_reads = true;
self.walk(node, depth);
}
}
}
fn walk(&mut self, node: &CompiledNode, depth: u32) {
match node {
CompiledNode::Value { .. } => {}
CompiledNode::Var {
scope_level,
segments,
metadata_hint,
default_value,
..
} => {
self.operators.insert(OpCode::Val.as_str());
if *metadata_hint == MetadataHint::None {
self.read(depth, *scope_level, segments);
}
if let Some(default) = default_value {
self.walk(default, depth);
}
}
#[cfg(feature = "ext-control")]
CompiledNode::Exists(data) => {
self.operators.insert(OpCode::Exists.as_str());
if !data.segments.is_empty() {
self.read(depth, data.scope_level, &data.segments);
}
}
#[cfg(feature = "error-handling")]
CompiledNode::Throw(_) => {
self.operators.insert(OpCode::Throw.as_str());
}
CompiledNode::Missing(data) => {
self.operators.insert(OpCode::Missing.as_str());
for arg in data.args.iter() {
match arg {
CompiledMissingArg::Now((_, segments)) => {
self.missing_path(depth, segments)
}
CompiledMissingArg::Later(node) => self.dynamic_missing_paths(node, depth),
}
}
}
CompiledNode::MissingSome(data) => {
self.operators.insert(OpCode::MissingSome.as_str());
if let CompiledMissingMin::Later(node) = &data.min_present {
self.walk(node, depth);
}
match &data.paths {
CompiledMissingPaths::Now(paths) => {
for (_, segments) in paths.iter() {
self.missing_path(depth, segments);
}
}
CompiledMissingPaths::Later(node) => self.dynamic_missing_paths(node, depth),
}
}
CompiledNode::InvalidArgs { op_name, .. } => {
if let Ok(op) = op_name.parse::<OpCode>() {
self.operators.insert(canonical_name(op));
}
}
CompiledNode::CustomOperator(data) => {
self.custom_operators.insert(data.name.clone());
self.nondeterministic |= !data.info.deterministic;
self.context_readers |= data.info.reads_context;
for arg in data.args.iter() {
self.walk(arg, depth);
}
}
CompiledNode::BuiltinOperator { opcode, args, .. } => {
self.builtin(*opcode, args, depth);
}
_ => node.visit_indexed_children(&mut |_, child| self.walk(child, depth)),
}
}
fn builtin(&mut self, opcode: OpCode, args: &[CompiledNode], depth: u32) {
let meta = opcode.meta();
self.operators.insert(canonical_name(opcode));
if meta.effect == Effect::Clock {
self.nondeterministic = true;
}
if meta.reads_context {
match opcode {
#[cfg(feature = "flagd")]
OpCode::Fractional => {
for path in [&["targetingKey"][..], &["$flagd", "flagKey"]] {
self.reads.insert(DataPath {
segments: path.iter().map(|s| s.to_string()).collect(),
});
}
}
_ => self.computed_reads = true,
}
}
let len = args.len();
for (index, arg) in args.iter().enumerate() {
self.walk(arg, depth + frames_pushed_for_child(opcode, index, len));
}
}
fn finish(self) -> Facts {
let mut reads: Vec<DataPath> = Vec::with_capacity(self.reads.len());
for path in self.reads {
if reads.last().is_none_or(|kept| !kept.covers(&path)) {
reads.push(path);
}
}
Facts {
reads,
computed_reads: self.computed_reads,
operators: self.operators.into_iter().collect(),
custom_operators: self.custom_operators.into_iter().collect(),
context_readers: self.context_readers,
deterministic: !self.nondeterministic,
}
}
}
fn canonical_name(op: OpCode) -> &'static str {
match op.catalogue_entry().names.first() {
Some(name) => name,
None => op
.as_str()
.parse::<OpCode>()
.map_or(op.as_str(), OpCode::as_str),
}
}
pub(crate) fn collect(root: &CompiledNode) -> Facts {
let mut collector = Collector::default();
collector.walk(root, 0);
collector.finish()
}