Skip to main content

reifydb_evaluate/
stack.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{collections::HashMap, sync::Arc};
5
6use arrow_array::{ArrayRef, RecordBatch};
7use arrow_schema::FieldRef;
8use reifydb_core::{
9	internal,
10	value::batch::{is_scalar, single_row},
11};
12use reifydb_rql::{
13	instruction::{CompiledClosure, CompiledFunction, Instruction},
14	nodes::FunctionParameter,
15};
16use reifydb_value::{
17	error,
18	value::{Value, constraint::TypeConstraint, system_columns::user_columns},
19};
20
21use crate::{Result, error::EvaluateError};
22
23pub fn strip_dollar_prefix(name: &str) -> &str {
24	name.strip_prefix('$').unwrap_or(name)
25}
26
27#[derive(Debug, Clone)]
28pub struct ClosureValue {
29	pub def: CompiledClosure,
30	pub captured: HashMap<String, Variable>,
31}
32
33#[derive(Debug, Clone)]
34pub struct Callable {
35	pub parameters: Vec<FunctionParameter>,
36	pub body: Vec<Instruction>,
37	pub captured: HashMap<String, Variable>,
38	pub return_type: Option<TypeConstraint>,
39}
40
41#[derive(Debug, Clone)]
42pub enum Variable {
43	Columns {
44		batch: RecordBatch,
45	},
46
47	ForIterator {
48		batch: RecordBatch,
49		index: usize,
50	},
51
52	Closure(ClosureValue),
53}
54
55impl Variable {
56	pub fn scalar(value: Value) -> Self {
57		Self::scalar_named("value", value)
58	}
59
60	pub fn scalar_named(name: &str, value: Value) -> Self {
61		Variable::Columns {
62			batch: single_row([(name, value)]).expect("one value always forms a one row batch"),
63		}
64	}
65
66	pub fn columns(batch: RecordBatch) -> Self {
67		Variable::Columns {
68			batch,
69		}
70	}
71
72	pub fn is_scalar(&self) -> bool {
73		matches!(
74			self,
75			Variable::Columns { batch } if is_scalar(batch)
76		)
77	}
78
79	pub fn into_column(self) -> Result<(FieldRef, ArrayRef)> {
80		let batch = match self {
81			Variable::Columns {
82				batch,
83				..
84			}
85			| Variable::ForIterator {
86				batch,
87				..
88			} => batch,
89			Variable::Closure(_) => single_row([("value", Value::none())])?,
90		};
91		let user: Vec<(&FieldRef, &ArrayRef)> = user_columns(&batch).collect();
92		let actual = user.len();
93		if let [(field, array)] = user.as_slice() {
94			Ok(((*field).clone(), (*array).clone()))
95		} else {
96			Err(error::TypeError::Runtime {
97				kind: error::RuntimeErrorKind::ExpectedSingleColumn {
98					actual,
99				},
100				message: format!("Expected a single column but got {}", actual),
101			}
102			.into())
103		}
104	}
105}
106
107#[derive(Debug, Clone)]
108pub struct SymbolTable {
109	inner: Arc<SymbolTableInner>,
110}
111
112#[derive(Debug, Clone)]
113struct SymbolTableInner {
114	scopes: Vec<Scope>,
115
116	functions: HashMap<String, CompiledFunction>,
117}
118
119#[derive(Debug, Clone)]
120struct Scope {
121	variables: HashMap<String, VariableBinding>,
122}
123
124#[derive(Debug, Clone)]
125struct VariableBinding {
126	variable: Variable,
127	mutable: bool,
128}
129
130impl SymbolTable {
131	pub fn new() -> Self {
132		let global_scope = Scope {
133			variables: HashMap::new(),
134		};
135
136		Self {
137			inner: Arc::new(SymbolTableInner {
138				scopes: vec![global_scope],
139				functions: HashMap::new(),
140			}),
141		}
142	}
143
144	pub fn enter_scope(&mut self) {
145		let new_scope = Scope {
146			variables: HashMap::new(),
147		};
148		Arc::make_mut(&mut self.inner).scopes.push(new_scope);
149	}
150
151	pub fn exit_scope(&mut self) -> Result<()> {
152		if self.inner.scopes.len() <= 1 {
153			return Err(error!(internal!("Cannot exit global scope")));
154		}
155		Arc::make_mut(&mut self.inner).scopes.pop();
156		Ok(())
157	}
158
159	pub fn set(&mut self, name: String, variable: Variable, mutable: bool) -> Result<()> {
160		self.set_in_current_scope(name, variable, mutable)
161	}
162
163	pub fn reassign(&mut self, name: String, variable: Variable) -> Result<()> {
164		let inner = Arc::make_mut(&mut self.inner);
165
166		for scope in inner.scopes.iter_mut().rev() {
167			if let Some(existing) = scope.variables.get(&name) {
168				if !existing.mutable {
169					return Err(EvaluateError::VariableIsImmutable {
170						name: name.clone(),
171					}
172					.into());
173				}
174				let mutable = existing.mutable;
175				scope.variables.insert(
176					name,
177					VariableBinding {
178						variable,
179						mutable,
180					},
181				);
182				return Ok(());
183			}
184		}
185
186		Err(EvaluateError::VariableNotFound {
187			name: name.clone(),
188		}
189		.into())
190	}
191
192	pub fn set_in_current_scope(&mut self, name: String, variable: Variable, mutable: bool) -> Result<()> {
193		let inner = Arc::make_mut(&mut self.inner);
194		let current_scope = inner.scopes.last_mut().unwrap();
195
196		current_scope.variables.insert(
197			name,
198			VariableBinding {
199				variable,
200				mutable,
201			},
202		);
203		Ok(())
204	}
205
206	pub fn get(&self, name: &str) -> Option<&Variable> {
207		for scope in self.inner.scopes.iter().rev() {
208			if let Some(binding) = scope.variables.get(name) {
209				return Some(&binding.variable);
210			}
211		}
212		None
213	}
214
215	pub fn define_function(&mut self, name: String, func: CompiledFunction) {
216		Arc::make_mut(&mut self.inner).functions.insert(name, func);
217	}
218
219	pub fn get_function(&self, name: &str) -> Option<&CompiledFunction> {
220		self.inner.functions.get(name)
221	}
222
223	pub fn resolve_callable(&self, name: &str) -> Option<Callable> {
224		if let Some(func) = self.get_function(name) {
225			return Some(Callable {
226				parameters: func.parameters.clone(),
227				body: func.body.clone(),
228				captured: HashMap::new(),
229				return_type: func.return_type.clone(),
230			});
231		}
232		if let Some(Variable::Closure(closure)) = self.get(strip_dollar_prefix(name)) {
233			return Some(Callable {
234				parameters: closure.def.parameters.clone(),
235				body: closure.def.body.clone(),
236				captured: closure.captured.clone(),
237				return_type: None,
238			});
239		}
240		None
241	}
242}
243
244impl Default for SymbolTable {
245	fn default() -> Self {
246		Self::new()
247	}
248}
249
250#[cfg(test)]
251pub mod tests {
252	use reifydb_core::value::{
253		batch::batch,
254		column::{builder::ColumnBuilder, factory::none_typed},
255	};
256	use reifydb_value::value::{Value, column_view::ColumnView, value_type::ValueType};
257
258	use super::*;
259
260	fn create_test_columns(values: Vec<Value>) -> RecordBatch {
261		if values.is_empty() {
262			return batch(vec![none_typed("test_col", ValueType::Boolean, 0)]).unwrap();
263		}
264
265		let empty = none_typed("test_col", values[0].get_type(), 0);
266		let mut builder = ColumnBuilder::from_view(&ColumnView::try_from(&empty).unwrap());
267		for value in values {
268			builder.push_value(value);
269		}
270
271		batch(vec![builder.finish("test_col")]).unwrap()
272	}
273
274	#[test]
275	fn test_basic_variable_operations() {
276		let mut ctx = SymbolTable::new();
277		let cols = create_test_columns(vec![Value::utf8("Alice".to_string())]);
278
279		ctx.set("name".to_string(), Variable::columns(cols.clone()), false).unwrap();
280
281		assert!(ctx.get("name").is_some());
282	}
283
284	#[test]
285	fn test_mutable_variable() {
286		let mut ctx = SymbolTable::new();
287		let cols1 = create_test_columns(vec![Value::Int4(42)]);
288		let cols2 = create_test_columns(vec![Value::Int4(84)]);
289
290		ctx.set("counter".to_string(), Variable::columns(cols1.clone()), true).unwrap();
291		assert!(ctx.get("counter").is_some());
292
293		ctx.set("counter".to_string(), Variable::columns(cols2.clone()), true).unwrap();
294		assert!(ctx.get("counter").is_some());
295	}
296
297	#[test]
298	#[ignore]
299	fn test_immutable_variable_reassignment_fails() {
300		let mut ctx = SymbolTable::new();
301		let cols1 = create_test_columns(vec![Value::utf8("Alice".to_string())]);
302		let cols2 = create_test_columns(vec![Value::utf8("Bob".to_string())]);
303
304		ctx.set("name".to_string(), Variable::columns(cols1.clone()), false).unwrap();
305
306		let result = ctx.set("name".to_string(), Variable::columns(cols2), false);
307		assert!(result.is_err());
308
309		// A refused reassignment must leave the original binding intact.
310		assert!(ctx.get("name").is_some());
311	}
312
313	#[test]
314	fn test_scope_management() {
315		let mut ctx = SymbolTable::new();
316
317		ctx.enter_scope();
318		ctx.enter_scope();
319		ctx.exit_scope().unwrap();
320		ctx.exit_scope().unwrap();
321
322		// Popping the global scope would leave the table with nowhere to bind.
323		assert!(ctx.exit_scope().is_err());
324	}
325
326	#[test]
327	fn test_variable_shadowing() {
328		let mut ctx = SymbolTable::new();
329		let outer_cols = create_test_columns(vec![Value::utf8("outer".to_string())]);
330		let inner_cols = create_test_columns(vec![Value::utf8("inner".to_string())]);
331
332		ctx.set("var".to_string(), Variable::columns(outer_cols.clone()), false).unwrap();
333		assert!(ctx.get("var").is_some());
334
335		// Rebinding an existing name in an inner scope must shadow, not overwrite.
336		ctx.enter_scope();
337		ctx.set("var".to_string(), Variable::columns(inner_cols.clone()), false).unwrap();
338
339		assert!(ctx.get("var").is_some());
340
341		ctx.exit_scope().unwrap();
342		assert!(ctx.get("var").is_some());
343	}
344
345	#[test]
346	fn test_parent_scope_access() {
347		let mut ctx = SymbolTable::new();
348		let outer_cols = create_test_columns(vec![Value::utf8("outer".to_string())]);
349
350		ctx.set("global_var".to_string(), Variable::columns(outer_cols.clone()), false).unwrap();
351
352		ctx.enter_scope();
353
354		// A function scope must read through to its parent.
355		assert!(ctx.get("global_var").is_some());
356	}
357
358	#[test]
359	fn test_nonexistent_variable() {
360		let ctx = SymbolTable::new();
361
362		assert!(ctx.get("nonexistent").is_none());
363	}
364}