1use std::sync::Arc;
15
16use panproto_expr::{BuiltinOp, EvalConfig, Expr, Literal};
17use panproto_gat::Name;
18
19use crate::element_ops::ElementOps;
20use crate::value::Value;
21use crate::wtype::WInstance;
22
23pub fn eval_with_instance(
36 expr: &Expr,
37 env: &panproto_expr::Env,
38 config: &EvalConfig,
39 instance: &WInstance,
40 context_node_id: Option<u32>,
41) -> Result<Literal, panproto_expr::ExprError> {
42 match expr {
43 Expr::Builtin(op, args) if is_graph_builtin(*op) => {
44 let mut eval_args = Vec::with_capacity(args.len());
46 for arg in args {
47 eval_args.push(eval_with_instance(
48 arg,
49 env,
50 config,
51 instance,
52 context_node_id,
53 )?);
54 }
55 apply_graph_builtin(*op, &eval_args, instance, context_node_id)
56 }
57 _ => panproto_expr::eval(expr, env, config),
58 }
59}
60
61pub fn eval_with_element_ops<T: ElementOps>(
72 expr: &Expr,
73 env: &panproto_expr::Env,
74 config: &EvalConfig,
75 instance: &T,
76 context: Option<u32>,
77) -> Result<Literal, panproto_expr::ExprError> {
78 match expr {
79 Expr::Builtin(op, args) if is_graph_builtin(*op) => {
80 let mut eval_args = Vec::with_capacity(args.len());
81 for arg in args {
82 eval_args.push(eval_with_element_ops(arg, env, config, instance, context)?);
83 }
84 instance.eval_graph_builtin(*op, &eval_args, context)
85 }
86 _ => panproto_expr::eval(expr, env, config),
87 }
88}
89
90const fn is_graph_builtin(op: BuiltinOp) -> bool {
92 matches!(
93 op,
94 BuiltinOp::Edge
95 | BuiltinOp::Children
96 | BuiltinOp::HasEdge
97 | BuiltinOp::EdgeCount
98 | BuiltinOp::Anchor
99 )
100}
101
102fn apply_graph_builtin(
104 op: BuiltinOp,
105 args: &[Literal],
106 instance: &WInstance,
107 context_node_id: Option<u32>,
108) -> Result<Literal, panproto_expr::ExprError> {
109 match op {
110 BuiltinOp::Edge => {
111 let node_id = resolve_node_ref(&args[0], context_node_id)?;
113 let edge_kind =
114 args[1]
115 .as_str()
116 .ok_or_else(|| panproto_expr::ExprError::TypeError {
117 expected: "string".into(),
118 got: args[1].type_name().into(),
119 })?;
120 let edge_name = Name::from(edge_kind);
121 for &(src, tgt, ref edge) in &instance.arcs {
123 if src == node_id && edge.kind == edge_name {
124 return Ok(node_to_literal(instance, tgt));
125 }
126 }
127 Ok(Literal::Null)
128 }
129 BuiltinOp::Children => {
130 let node_id = resolve_node_ref(&args[0], context_node_id)?;
132 let mut children = Vec::new();
133 for &(src, tgt, _) in &instance.arcs {
134 if src == node_id {
135 children.push(node_to_literal(instance, tgt));
136 }
137 }
138 Ok(Literal::List(children))
139 }
140 BuiltinOp::HasEdge => {
141 let node_id = resolve_node_ref(&args[0], context_node_id)?;
143 let edge_kind =
144 args[1]
145 .as_str()
146 .ok_or_else(|| panproto_expr::ExprError::TypeError {
147 expected: "string".into(),
148 got: args[1].type_name().into(),
149 })?;
150 let edge_name = Name::from(edge_kind);
151 let found = instance
152 .arcs
153 .iter()
154 .any(|(src, _, edge)| *src == node_id && edge.kind == edge_name);
155 Ok(Literal::Bool(found))
156 }
157 BuiltinOp::EdgeCount => {
158 let node_id = resolve_node_ref(&args[0], context_node_id)?;
160 let count = instance
161 .arcs
162 .iter()
163 .filter(|(src, _, _)| *src == node_id)
164 .count();
165 #[allow(clippy::cast_possible_wrap)]
166 Ok(Literal::Int(count as i64))
167 }
168 BuiltinOp::Anchor => {
169 let node_id = resolve_node_ref(&args[0], context_node_id)?;
171 instance
172 .nodes
173 .get(&node_id)
174 .map_or(Ok(Literal::Null), |node| {
175 Ok(Literal::Str(node.anchor.as_ref().into()))
176 })
177 }
178 _ => Ok(Literal::Null),
179 }
180}
181
182fn resolve_node_ref(
187 lit: &Literal,
188 context_node_id: Option<u32>,
189) -> Result<u32, panproto_expr::ExprError> {
190 match lit {
191 Literal::Int(id) => u32::try_from(*id).map_err(|_| panproto_expr::ExprError::TypeError {
192 expected: "non-negative int fitting u32".into(),
193 got: format!("{id}"),
194 }),
195 Literal::Str(s) if s == "self" => context_node_id.ok_or_else(|| {
196 panproto_expr::ExprError::UnboundVariable("self (no context node)".into())
197 }),
198 _ => Err(panproto_expr::ExprError::TypeError {
199 expected: "int or \"self\"".into(),
200 got: lit.type_name().into(),
201 }),
202 }
203}
204
205fn node_to_literal(instance: &WInstance, node_id: u32) -> Literal {
209 let Some(node) = instance.nodes.get(&node_id) else {
210 return Literal::Null;
211 };
212 let mut fields: Vec<(Arc<str>, Literal)> = Vec::new();
213 fields.push((Arc::from("_id"), Literal::Int(i64::from(node.id))));
214 fields.push((
215 Arc::from("_anchor"),
216 Literal::Str(node.anchor.as_ref().into()),
217 ));
218 for (key, val) in &node.extra_fields {
219 fields.push((Arc::from(key.as_str()), value_to_literal(val)));
220 }
221 Literal::Record(fields)
222}
223
224fn value_to_literal(val: &Value) -> Literal {
226 crate::wtype::value_to_expr_literal(val)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use std::collections::HashMap;
233
234 use crate::metadata::Node;
235 use crate::value::Value;
236 use panproto_schema::Edge as SchemaEdge;
237
238 fn make_instance() -> WInstance {
239 let mut nodes = HashMap::new();
240 let mut root = Node::new(0, "document");
241 root.extra_fields
242 .insert("title".into(), Value::Str("Test".into()));
243 nodes.insert(0, root);
244
245 let mut child = Node::new(1, "paragraph");
246 child
247 .extra_fields
248 .insert("text".into(), Value::Str("Hello".into()));
249 nodes.insert(1, child);
250
251 let edge = SchemaEdge {
252 src: Name::from("document"),
253 tgt: Name::from("paragraph"),
254 kind: Name::from("body"),
255 name: None,
256 };
257
258 WInstance::new(nodes, vec![(0, 1, edge)], vec![], 0, Name::from("document"))
259 }
260
261 fn eval_ok(expr: &Expr, inst: &WInstance, ctx: Option<u32>) -> Literal {
263 let env = panproto_expr::Env::new();
264 let config = EvalConfig::default();
265 let result = eval_with_instance(expr, &env, &config, inst, ctx);
266 assert!(result.is_ok(), "eval failed: {result:?}");
267 result.unwrap_or(Literal::Null)
268 }
269
270 #[test]
271 fn edge_follows_arc() {
272 let inst = make_instance();
273 let expr = Expr::Builtin(
274 BuiltinOp::Edge,
275 vec![
276 Expr::Lit(Literal::Int(0)),
277 Expr::Lit(Literal::Str("body".into())),
278 ],
279 );
280 let result = eval_ok(&expr, &inst, Some(0));
281 assert!(matches!(result, Literal::Record(_)));
282 }
283
284 #[test]
285 fn children_returns_list() {
286 let inst = make_instance();
287 let expr = Expr::Builtin(BuiltinOp::Children, vec![Expr::Lit(Literal::Int(0))]);
288 let result = eval_ok(&expr, &inst, Some(0));
289 assert!(matches!(result, Literal::List(ref items) if items.len() == 1));
290 }
291
292 #[test]
293 fn has_edge_true() {
294 let inst = make_instance();
295 let expr = Expr::Builtin(
296 BuiltinOp::HasEdge,
297 vec![
298 Expr::Lit(Literal::Int(0)),
299 Expr::Lit(Literal::Str("body".into())),
300 ],
301 );
302 assert_eq!(eval_ok(&expr, &inst, Some(0)), Literal::Bool(true));
303 }
304
305 #[test]
306 fn has_edge_false() {
307 let inst = make_instance();
308 let expr = Expr::Builtin(
309 BuiltinOp::HasEdge,
310 vec![
311 Expr::Lit(Literal::Int(0)),
312 Expr::Lit(Literal::Str("nonexistent".into())),
313 ],
314 );
315 assert_eq!(eval_ok(&expr, &inst, Some(0)), Literal::Bool(false));
316 }
317
318 #[test]
319 fn edge_count_works() {
320 let inst = make_instance();
321 let expr = Expr::Builtin(BuiltinOp::EdgeCount, vec![Expr::Lit(Literal::Int(0))]);
322 assert_eq!(eval_ok(&expr, &inst, Some(0)), Literal::Int(1));
323 }
324
325 #[test]
326 fn anchor_returns_kind() {
327 let inst = make_instance();
328 let expr = Expr::Builtin(BuiltinOp::Anchor, vec![Expr::Lit(Literal::Int(1))]);
329 assert_eq!(
330 eval_ok(&expr, &inst, Some(0)),
331 Literal::Str("paragraph".into())
332 );
333 }
334}