surrealdb_expr/expr/function_facts.rs
1//! Local, derivable facts about an expression's ability to modify data.
2//!
3//! [`Expr::contains_mutation`] answers "does this tree itself write?" as a
4//! single yes/no. This module extracts the three facts a *call-graph* answer
5//! needs instead: whether the tree writes directly, which user-defined
6//! functions it calls (their stored bodies carry the rest of the answer), and
7//! whether it invokes something whose body cannot be inspected at all. The
8//! caller combines these facts across function bodies with two polarities:
9//!
10//! - **Precise** ("provably writes"): only `direct_writes`, unioned over the reachable call graph.
11//! Opaque callables count as clean, because refusing them would refuse working read-only bodies
12//! (a pure script, a pure `eval`).
13//! - **Conservative** ("possibly writes"): `direct_writes` or `opaque_effects` anywhere reachable.
14//! This is the polarity a planner must use before treating an expression as read-only.
15//!
16//! Known envelope: a closure *value* that reaches an invocation site as data
17//! is invisible to this walk when the site itself looks harmless — as an
18//! argument to a closure-taking builtin (`array::map($arr, $fn)`), or stored
19//! on the receiver under a name that shadows a registered builtin method.
20//! This matches the executors' own access-mode analysis. The runtime write
21//! refusal and the transaction's own write gate remain the backstop for those
22//! shapes. Method calls whose name is *not* a registered builtin can only
23//! resolve to such a closure, so they count as opaque here.
24
25use std::collections::BTreeSet;
26
27use crate::expr::operator::PostfixOperator;
28use crate::expr::visit::{Visit as _, Visitor};
29use crate::expr::{Block, Expr, Function, Part};
30
31/// What one expression tree contributes to a mutability answer, before any
32/// callee bodies are consulted.
33#[derive(Clone, Debug, Default, PartialEq, Eq)]
34pub struct FunctionFacts {
35 /// A data-modifying statement appears somewhere in the tree itself: the
36 /// same construct set [`Expr::contains_mutation`] rejects, at the same
37 /// full depth (subqueries, blocks, idiom parts, closure bodies and call
38 /// arguments included).
39 pub direct_writes: bool,
40 /// Names of the user-defined functions the tree calls, without the
41 /// `fn::` prefix, at any depth.
42 pub calls: BTreeSet<String>,
43 /// The tree invokes something whose body cannot be inspected: a script,
44 /// module or silo function, a statement-evaluating builtin (the set
45 /// [`Function::read_only`] rejects), or a call operator whose target is
46 /// not a closure literal.
47 pub opaque_effects: bool,
48}
49
50/// Collects [`FunctionFacts`] over a whole tree. Unlike the mutation scanner
51/// this never short-circuits: `calls` must be complete even when a write has
52/// already been found, because the caller builds a call graph from it.
53struct FactsScanner {
54 facts: FunctionFacts,
55}
56
57impl Visitor for FactsScanner {
58 type Error = std::convert::Infallible;
59
60 /// The match is **exhaustive (no `_` arm) on purpose**, matching
61 /// [`crate::expr::mutation`]: adding an `Expr` variant should be a build
62 /// error here so a human classifies it against all three facts.
63 fn visit_expr(&mut self, expr: &Expr) -> Result<(), Self::Error> {
64 match expr {
65 // Data-modifying statements and DDL.
66 Expr::Create(_)
67 | Expr::Update(_)
68 | Expr::Upsert(_)
69 | Expr::Delete(_)
70 | Expr::Relate(_)
71 | Expr::Insert(_)
72 | Expr::Define(_)
73 | Expr::Remove(_)
74 | Expr::Rebuild(_)
75 | Expr::Alter(_) => {
76 self.facts.direct_writes = true;
77 }
78
79 // A GQL plan carries its mutations in its stages.
80 Expr::Match(plan) => {
81 if plan.has_mutations() {
82 self.facts.direct_writes = true;
83 }
84 }
85
86 // A call operator on anything but a closure literal executes a
87 // body this tree cannot see. A literal's body is walked by the
88 // default traversal, so it needs no special case.
89 Expr::Postfix {
90 expr: target,
91 op: PostfixOperator::Call(_),
92 } => {
93 if !matches!(&**target, Expr::Closure(_)) {
94 self.facts.opaque_effects = true;
95 }
96 }
97
98 // A method call whose name is not a registered builtin resolves
99 // to a closure stored on the receiver, whose body this tree
100 // cannot see. `Part::Method` is the idiom-path shape of the same
101 // call and is classified in `visit_part`.
102 Expr::Postfix {
103 op: PostfixOperator::MethodCall(name, _),
104 ..
105 } => {
106 if !crate::expr::method::is_builtin_method(name) {
107 self.facts.opaque_effects = true;
108 }
109 }
110
111 // Everything else contributes only what the default traversal
112 // finds in its children: blocks, subqueries, idiom parts, closure
113 // bodies and call arguments are all descended into.
114 Expr::Literal(_)
115 | Expr::Param(_)
116 | Expr::Idiom(_)
117 | Expr::Table(_)
118 | Expr::Mock(_)
119 | Expr::Block(_)
120 | Expr::Constant(_)
121 | Expr::Prefix {
122 ..
123 }
124 | Expr::Postfix {
125 ..
126 }
127 | Expr::Binary {
128 ..
129 }
130 | Expr::FunctionCall(_)
131 | Expr::Closure(_)
132 | Expr::Break
133 | Expr::Continue
134 | Expr::Return(_)
135 | Expr::Throw(_)
136 | Expr::IfElse(_)
137 | Expr::Select(_)
138 | Expr::Info(_)
139 | Expr::Foreach(_)
140 | Expr::Let(_)
141 | Expr::Sleep(_)
142 | Expr::Explain {
143 ..
144 } => {}
145 }
146 expr.visit(self)
147 }
148
149 /// The idiom-path shape of a method call: an unregistered name resolves
150 /// to a closure stored on the receiver, whose body this tree cannot see.
151 fn visit_part(&mut self, part: &Part) -> Result<(), Self::Error> {
152 if let Part::Method(name, _) = part
153 && !crate::expr::method::is_builtin_method(name)
154 {
155 self.facts.opaque_effects = true;
156 }
157 part.visit(self)
158 }
159
160 fn visit_function(&mut self, f: &Function) -> Result<(), Self::Error> {
161 match f {
162 // The callee's stored body carries the answer; record the edge.
163 Function::Custom(name) => {
164 self.facts.calls.insert(name.clone());
165 }
166 // Builtins, scripts, models, modules and silos: reuse the
167 // transaction-type classification as the single source of truth
168 // for which of them can evaluate arbitrary statements.
169 other => {
170 if !other.read_only() {
171 self.facts.opaque_effects = true;
172 }
173 }
174 }
175 Ok(())
176 }
177}
178
179impl Expr {
180 /// Extract this tree's [`FunctionFacts`].
181 pub fn function_facts(&self) -> FunctionFacts {
182 let mut scanner = FactsScanner {
183 facts: FunctionFacts::default(),
184 };
185 // Enter through the visitor method, not the default traversal, so the
186 // root node is classified too. Infallible: the scanner accumulates.
187 let Ok(()) = scanner.visit_expr(self);
188 scanner.facts
189 }
190}
191
192impl Block {
193 /// Extract this block's [`FunctionFacts`], the form stored function
194 /// bodies take.
195 pub fn function_facts(&self) -> FunctionFacts {
196 let mut scanner = FactsScanner {
197 facts: FunctionFacts::default(),
198 };
199 let Ok(()) = scanner.visit_block(self);
200 scanner.facts
201 }
202}