use std::collections::HashSet;
use rhai::{ASTNode, Expr, Stmt};
use crate::graph::{AccessRequest, AccessRequestBuf, Action, ElementKind, Resource};
use crate::traversal::step::AccessFlow;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct BodyEffects {
pub(crate) writes: Vec<AccessRequestBuf>,
pub(crate) calls: Vec<String>,
pub(crate) dynamic: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StepClass {
Vertices,
EdgesLabelled,
Edges,
AddV,
AddE,
Merge(ElementKind),
Update,
Relabel(ElementKind),
Drop,
HasLabel,
Keep,
KeepPlain,
Query,
SideEffectTarget,
Dynamic,
Refused(&'static str),
}
const SCHEMA: &str = "it changes the schema";
const CATALOG: &str = "it changes the catalog (saved queries, indexes, compression rules)";
const FILES: &str = "it reads or writes a file";
pub(crate) const CLASSES: &[(&[&str], StepClass)] = {
use StepClass::*;
&[
(
&[
"v", "V", "out", "in", "in_", "both", "out_v", "outV", "in_v", "inV", "both_v",
"bothV", "other_v", "otherV",
],
Vertices,
),
(
&["out_e", "outE", "in_e", "inE", "both_e", "bothE"],
EdgesLabelled,
),
(&["e", "E"], Edges),
(&["add_v", "addV"], AddV),
(&["add_e", "addE"], AddE),
(&["merge_v", "mergeV"], Merge(ElementKind::Vertex)),
(&["merge_e", "mergeE"], Merge(ElementKind::Edge)),
(
&[
"property",
"property_json",
"propertyJson",
"remove_property",
"removeProperty",
],
Update,
),
(
&["add_label", "addLabel", "drop_label", "dropLabel"],
Relabel(ElementKind::Vertex),
),
(&["set_label", "setLabel"], Relabel(ElementKind::Edge)),
(&["drop"], Drop),
(&["has_label", "hasLabel"], HasLabel),
(
&[
"has",
"has_not",
"hasNot",
"has_id",
"hasId",
"has_key",
"hasKey",
"has_value",
"hasValue",
"has_p",
"is",
"is_",
"where",
"where_p",
"where_t",
"filter",
"not",
"and",
"or",
"simple_path",
"simplePath",
"cyclic_path",
"cyclicPath",
"as",
"as_",
"by",
"from",
"to",
"identity",
"barrier",
"aggregate",
"store",
"side_effect",
"sideEffect",
],
Keep,
),
(
&["dedup", "limit", "order", "range", "sample", "skip", "tail"],
KeepPlain,
),
(&["query"], Query),
(&["with_side_effect", "withSideEffect"], SideEffectTarget),
(&["Fn", "call", "curry", "eval"], Dynamic),
(
&[
"set_schema",
"setSchema",
"patch_schema",
"patchSchema",
"embed_property",
"embedProperty",
],
Refused(SCHEMA),
),
(
&[
"define_query",
"defineQuery",
"define_queries",
"defineQueries",
"drop_query",
"dropQuery",
"move_query",
"moveQuery",
"describe_query",
"describeQuery",
"create_index",
"createIndex",
"drop_index",
"dropIndex",
"define_compression",
"defineCompression",
"drop_compression",
"dropCompression",
"recompress",
],
Refused(CATALOG),
),
(
&[
"import_graphml",
"importGraphml",
"import_graphson",
"importGraphson",
"export_graphml",
"exportGraphml",
"export_graphson",
"exportGraphson",
"export_snapshot",
"exportSnapshot",
],
Refused(FILES),
),
(
&["mark"],
Refused("it writes a mark into the graph's journal"),
),
]
};
pub(crate) fn classify(name: &str) -> Option<StepClass> {
CLASSES
.iter()
.find(|(names, _)| names.contains(&name))
.map(|(_, class)| *class)
}
pub(crate) fn analyze(ast: &rhai::AST) -> Result<BodyEffects, String> {
let mut analysis = Analysis::default();
ast.walk(&mut |path: &[ASTNode]| {
let Some(node) = path.last() else {
return true;
};
let parent = path.len().checked_sub(2).and_then(|at| path.get(at));
match node {
ASTNode::Stmt(Stmt::Import(..)) => analysis.dynamic("import"),
ASTNode::Expr(expr @ Expr::Dot(..))
if !matches!(parent, Some(ASTNode::Expr(Expr::Dot(..)))) =>
{
analysis.chain(expr);
}
ASTNode::Expr(Expr::FnCall(call, _)) | ASTNode::Stmt(Stmt::FnCall(call, _)) => {
let args = call.args.get(1..).unwrap_or_default();
analysis.call(call.name.as_str(), args, &mut AccessFlow::default());
}
ASTNode::Expr(Expr::DynamicConstant(value, _)) => {
if let Some(pointer) = value.read_lock::<rhai::FnPtr>()
&& !pointer.is_anonymous()
{
analysis.dynamic(&format!("Fn(\"{}\")", pointer.fn_name()));
}
}
_ => {}
}
analysis.refused.is_none()
});
match analysis.refused {
Some(reason) => Err(reason),
None => Ok(analysis.effects),
}
}
#[derive(Default)]
struct Analysis {
effects: BodyEffects,
refused: Option<String>,
}
impl Analysis {
fn dynamic(&mut self, construct: &str) {
if self.effects.dynamic.is_none() {
self.effects.dynamic = Some(construct.to_string());
}
}
fn write(&mut self, request: AccessRequest<'_>) {
let request = request.to_owned_request();
if !self.effects.writes.contains(&request) {
self.effects.writes.push(request);
}
}
fn received(&mut self, flow: &AccessFlow, action: Action<'_>, element: Option<ElementKind>) {
let element = element.or(flow.element);
if flow.labels.is_empty() || element != flow.element {
return self.write(AccessRequest::new(action, Resource::data(element, None)));
}
for label in &flow.labels {
self.write(AccessRequest::new(
action,
Resource::data(element, Some(label.as_str())),
));
}
}
fn chain(&mut self, top: &Expr) {
let mut links = Vec::new();
flatten(top, &mut links);
let mut flow = AccessFlow::default();
for link in links.iter().skip(1) {
match link {
Expr::MethodCall(call, _) => self.call(call.name.as_str(), &call.args, &mut flow),
Expr::Index(binary, _, _) => {
if let Expr::MethodCall(call, _) = &binary.lhs {
self.call(call.name.as_str(), &call.args, &mut flow);
}
flow = AccessFlow::default();
}
_ => flow = AccessFlow::default(),
}
}
}
fn call(&mut self, name: &str, args: &[Expr], flow: &mut AccessFlow) {
use StepClass::*;
let Some(class) = classify(name) else {
*flow = AccessFlow::default();
return;
};
let vertex = Some(ElementKind::Vertex);
let edge = Some(ElementKind::Edge);
match class {
Vertices => *flow = of(ElementKind::Vertex, Vec::new()),
Edges => *flow = of(ElementKind::Edge, Vec::new()),
EdgesLabelled => {
*flow = of(ElementKind::Edge, literal_strings(args).unwrap_or_default());
}
AddV | AddE => {
let element = if class == AddV { vertex } else { edge };
let labels = literal_strings(args).unwrap_or_default();
if labels.is_empty() {
self.write(AccessRequest::new(
Action::Create,
Resource::data(element, None),
));
}
for label in &labels {
self.write(AccessRequest::new(
Action::Create,
Resource::data(element, Some(label.as_str())),
));
}
*flow = AccessFlow { element, labels };
}
Merge(kind) => {
for action in [Action::Create, Action::Update] {
self.write(AccessRequest::new(action, Resource::data(Some(kind), None)));
}
*flow = of(kind, Vec::new());
}
Update => self.received(flow, Action::Update, None),
Relabel(kind) => {
self.received(flow, Action::Update, Some(kind));
flow.labels.clear();
}
Drop => self.received(flow, Action::Delete, None),
HasLabel => {
if flow.element == edge
&& flow.labels.is_empty()
&& let Some(labels) = literal_strings(args).filter(|l| !l.is_empty())
{
flow.labels = labels;
}
}
Keep => {}
KeepPlain => {
if !args.iter().all(is_plain) {
*flow = AccessFlow::default();
}
}
Query => {
match args.first() {
Some(Expr::StringConstant(name, _)) => {
if !self
.effects
.calls
.iter()
.any(|known| known == name.as_str())
{
self.effects.calls.push(name.to_string());
}
}
_ => self.dynamic("g.query(name) with a computed name"),
}
*flow = AccessFlow::default();
}
SideEffectTarget => {
if args.len() > 1 && !args.iter().skip(1).all(is_constant) {
self.write(AccessRequest::new(
Action::Create,
Resource::data(None, None),
));
}
}
Dynamic => {
self.dynamic(name);
*flow = AccessFlow::default();
}
Refused(why) => {
if self.refused.is_none() {
self.refused = Some(format!(
"it calls {name}(), and {why}; a saved query may write data only"
));
}
}
}
}
}
fn of(element: ElementKind, labels: Vec<String>) -> AccessFlow {
AccessFlow {
element: Some(element),
labels,
}
}
fn flatten<'a>(expr: &'a Expr, links: &mut Vec<&'a Expr>) {
match expr {
Expr::Dot(binary, _, _) => {
flatten(&binary.lhs, links);
flatten(&binary.rhs, links);
}
other => links.push(other),
}
}
fn literal_strings(args: &[Expr]) -> Option<Vec<String>> {
let mut strings = Vec::new();
for arg in args {
match arg {
Expr::StringConstant(text, _) => strings.push(text.to_string()),
Expr::Array(items, _) => {
for item in items.iter() {
match item {
Expr::StringConstant(text, _) => strings.push(text.to_string()),
_ => return None,
}
}
}
Expr::DynamicConstant(value, _) => {
let items = value.read_lock::<rhai::Array>()?;
for item in items.iter() {
strings.push(item.read_lock::<rhai::ImmutableString>()?.to_string());
}
}
_ => return None,
}
}
Some(strings)
}
fn is_plain(arg: &Expr) -> bool {
matches!(
arg,
Expr::IntegerConstant(..) | Expr::StringConstant(..) | Expr::FloatConstant(..)
)
}
fn is_constant(arg: &Expr) -> bool {
match arg {
Expr::IntegerConstant(..)
| Expr::StringConstant(..)
| Expr::FloatConstant(..)
| Expr::BoolConstant(..)
| Expr::CharConstant(..)
| Expr::Unit(..) => true,
Expr::Array(items, _) => items.iter().all(is_constant),
Expr::Map(map, _) => map.0.iter().all(|(_, value)| is_constant(value)),
Expr::DynamicConstant(value, _) => !value.is_fnptr(),
_ => false,
}
}
pub(crate) fn covers(effect: &AccessRequestBuf, request: &AccessRequest<'_>) -> bool {
let effect = effect.as_request();
let (
Resource::Data {
element: effect_element,
label: effect_label,
},
Resource::Data { element, label },
) = (effect.resource, request.resource)
else {
return false;
};
effect.action.kind() == request.action.kind()
&& effect_element.is_none_or(|kind| element == Some(kind))
&& effect_label.is_none_or(|name| label == Some(name))
}
pub(crate) fn extend(effects: &mut Vec<AccessRequestBuf>, more: &[AccessRequestBuf]) {
for request in more {
if !effects.contains(request) {
effects.push(request.clone());
}
}
}
pub(crate) struct Node {
pub(crate) effects: BodyEffects,
pub(crate) counts: bool,
}
pub(crate) fn transitive(
name: &str,
lookup: &mut dyn FnMut(&str) -> Option<Node>,
) -> (Vec<AccessRequestBuf>, Option<String>) {
let mut effects = Vec::new();
let mut dynamic = None;
let mut seen: HashSet<String> = HashSet::new();
let mut pending = vec![(name.to_string(), true)];
while let Some((next, root)) = pending.pop() {
if !seen.insert(next.clone()) {
continue;
}
let Some(node) = lookup(&next) else {
continue;
};
if !root && !node.counts {
continue;
}
extend(&mut effects, &node.effects.writes);
if dynamic.is_none() {
dynamic = node.effects.dynamic.clone();
}
pending.extend(node.effects.calls.into_iter().map(|call| (call, false)));
}
(effects, dynamic)
}