Skip to main content

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}