1use std::sync::Arc;
7
8use crate::error::ExprError;
9use crate::expr::BuiltinOp;
10use crate::literal::Literal;
11
12pub fn apply_builtin(op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError> {
19 let expected = op.arity();
20 if args.len() != expected {
21 return Err(ExprError::ArityMismatch {
22 op: format!("{op:?}"),
23 expected,
24 got: args.len(),
25 });
26 }
27
28 match op {
29 BuiltinOp::Add
31 | BuiltinOp::Sub
32 | BuiltinOp::Mul
33 | BuiltinOp::Div
34 | BuiltinOp::Mod
35 | BuiltinOp::Neg
36 | BuiltinOp::Abs
37 | BuiltinOp::Floor
38 | BuiltinOp::Ceil => apply_arithmetic(op, args),
39
40 BuiltinOp::Eq
42 | BuiltinOp::Neq
43 | BuiltinOp::Lt
44 | BuiltinOp::Lte
45 | BuiltinOp::Gt
46 | BuiltinOp::Gte => apply_comparison(op, args),
47
48 BuiltinOp::And | BuiltinOp::Or | BuiltinOp::Not => apply_boolean(op, args),
50
51 BuiltinOp::Concat
53 | BuiltinOp::Len
54 | BuiltinOp::Slice
55 | BuiltinOp::Upper
56 | BuiltinOp::Lower
57 | BuiltinOp::Trim
58 | BuiltinOp::Split
59 | BuiltinOp::Join
60 | BuiltinOp::Replace
61 | BuiltinOp::Contains => apply_string(op, args),
62
63 BuiltinOp::Map
65 | BuiltinOp::Filter
66 | BuiltinOp::Fold
67 | BuiltinOp::FlatMap
68 | BuiltinOp::Append
69 | BuiltinOp::Head
70 | BuiltinOp::Tail
71 | BuiltinOp::Reverse
72 | BuiltinOp::Length => apply_list(op, args),
73
74 BuiltinOp::MergeRecords | BuiltinOp::Keys | BuiltinOp::Values | BuiltinOp::HasField => {
76 apply_record(op, args)
77 }
78
79 BuiltinOp::IntToFloat
81 | BuiltinOp::FloatToInt
82 | BuiltinOp::IntToStr
83 | BuiltinOp::FloatToStr
84 | BuiltinOp::StrToInt
85 | BuiltinOp::StrToFloat => apply_coercion(op, args),
86
87 BuiltinOp::TypeOf | BuiltinOp::IsNull | BuiltinOp::IsList => apply_inspection(op, args),
89 BuiltinOp::Edge
93 | BuiltinOp::Children
94 | BuiltinOp::HasEdge
95 | BuiltinOp::EdgeCount
96 | BuiltinOp::Anchor => Ok(Literal::Null),
97 }
98}
99
100fn apply_arithmetic(op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError> {
102 match op {
103 BuiltinOp::Add => numeric_binop(&args[0], &args[1], i64::checked_add, |a, b| a + b),
104 BuiltinOp::Sub => numeric_binop(&args[0], &args[1], i64::checked_sub, |a, b| a - b),
105 BuiltinOp::Mul => numeric_binop(&args[0], &args[1], i64::checked_mul, |a, b| a * b),
106 BuiltinOp::Div => {
107 let is_zero = match (&args[0], &args[1]) {
108 (_, Literal::Int(0)) => true,
109 (_, Literal::Float(b)) if *b == 0.0 => true,
110 _ => false,
111 };
112 if is_zero {
113 Err(ExprError::DivisionByZero)
114 } else {
115 numeric_binop(&args[0], &args[1], i64::checked_div, |a, b| a / b)
116 }
117 }
118 BuiltinOp::Mod => match (&args[0], &args[1]) {
119 (Literal::Int(_), Literal::Int(0)) => Err(ExprError::DivisionByZero),
120 (Literal::Int(a), Literal::Int(b)) => Ok(Literal::Int(a % b)),
121 _ => Err(type_err("int", &args[0])),
122 },
123 BuiltinOp::Neg => match &args[0] {
124 Literal::Int(n) => Ok(Literal::Int(-n)),
125 Literal::Float(f) => Ok(Literal::Float(-f)),
126 other => Err(type_err("int|float", other)),
127 },
128 BuiltinOp::Abs => match &args[0] {
129 Literal::Int(n) => Ok(Literal::Int(n.abs())),
130 Literal::Float(f) => Ok(Literal::Float(f.abs())),
131 other => Err(type_err("int|float", other)),
132 },
133 #[allow(clippy::cast_possible_truncation)]
134 BuiltinOp::Floor => match &args[0] {
135 Literal::Float(f) => Ok(Literal::Int(f.floor() as i64)),
136 other => Err(type_err("float", other)),
137 },
138 #[allow(clippy::cast_possible_truncation)]
139 BuiltinOp::Ceil => match &args[0] {
140 Literal::Float(f) => Ok(Literal::Int(f.ceil() as i64)),
141 other => Err(type_err("float", other)),
142 },
143 _ => unreachable!(),
144 }
145}
146
147fn apply_comparison(op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError> {
149 match op {
150 BuiltinOp::Eq => Ok(Literal::Bool(args[0] == args[1])),
151 BuiltinOp::Neq => Ok(Literal::Bool(args[0] != args[1])),
152 BuiltinOp::Lt => compare(&args[0], &args[1], std::cmp::Ordering::is_lt),
153 BuiltinOp::Lte => compare(&args[0], &args[1], std::cmp::Ordering::is_le),
154 BuiltinOp::Gt => compare(&args[0], &args[1], std::cmp::Ordering::is_gt),
155 BuiltinOp::Gte => compare(&args[0], &args[1], std::cmp::Ordering::is_ge),
156 _ => unreachable!(),
157 }
158}
159
160fn apply_boolean(op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError> {
162 match op {
163 BuiltinOp::And => match (&args[0], &args[1]) {
164 (Literal::Bool(a), Literal::Bool(b)) => Ok(Literal::Bool(*a && *b)),
165 (Literal::Bool(_), other) | (other, _) => Err(type_err("bool", other)),
166 },
167 BuiltinOp::Or => match (&args[0], &args[1]) {
168 (Literal::Bool(a), Literal::Bool(b)) => Ok(Literal::Bool(*a || *b)),
169 (Literal::Bool(_), other) | (other, _) => Err(type_err("bool", other)),
170 },
171 BuiltinOp::Not => match &args[0] {
172 Literal::Bool(b) => Ok(Literal::Bool(!b)),
173 other => Err(type_err("bool", other)),
174 },
175 _ => unreachable!(),
176 }
177}
178
179#[allow(
181 clippy::cast_possible_truncation,
182 clippy::cast_possible_wrap,
183 clippy::cast_sign_loss
184)]
185fn apply_string(op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError> {
186 match op {
187 BuiltinOp::Concat => match (&args[0], &args[1]) {
188 (Literal::Str(a), Literal::Str(b)) => {
189 let mut s = a.clone();
190 s.push_str(b);
191 Ok(Literal::Str(s))
192 }
193 (Literal::Str(_), other) | (other, _) => Err(type_err("string", other)),
194 },
195 BuiltinOp::Len => match &args[0] {
196 Literal::Str(s) => Ok(Literal::Int(s.len() as i64)),
197 other => Err(type_err("string", other)),
198 },
199 BuiltinOp::Slice => match (&args[0], &args[1], &args[2]) {
200 (Literal::Str(s), Literal::Int(start), Literal::Int(end)) => {
201 let start = (*start).max(0) as usize;
202 let end = (*end).max(0) as usize;
203 let end = end.min(s.len());
204 let start = start.min(end);
205 Ok(Literal::Str(s[start..end].to_string()))
206 }
207 _ => Err(type_err("(string, int, int)", &args[0])),
208 },
209 BuiltinOp::Upper => match &args[0] {
210 Literal::Str(s) => Ok(Literal::Str(s.to_uppercase())),
211 other => Err(type_err("string", other)),
212 },
213 BuiltinOp::Lower => match &args[0] {
214 Literal::Str(s) => Ok(Literal::Str(s.to_lowercase())),
215 other => Err(type_err("string", other)),
216 },
217 BuiltinOp::Trim => match &args[0] {
218 Literal::Str(s) => Ok(Literal::Str(s.trim().to_string())),
219 other => Err(type_err("string", other)),
220 },
221 BuiltinOp::Split => match (&args[0], &args[1]) {
222 (Literal::Str(s), Literal::Str(delim)) => Ok(Literal::List(
223 s.split(&**delim)
224 .map(|p| Literal::Str(p.to_string()))
225 .collect(),
226 )),
227 _ => Err(type_err("(string, string)", &args[0])),
228 },
229 BuiltinOp::Join => match (&args[0], &args[1]) {
230 (Literal::List(parts), Literal::Str(delim)) => {
231 let strs: Result<Vec<_>, _> = parts
232 .iter()
233 .map(|p| match p {
234 Literal::Str(s) => Ok(s.as_str()),
235 other => Err(type_err("string", other)),
236 })
237 .collect();
238 Ok(Literal::Str(strs?.join(delim)))
239 }
240 _ => Err(type_err("([string], string)", &args[0])),
241 },
242 BuiltinOp::Replace => match (&args[0], &args[1], &args[2]) {
243 (Literal::Str(s), Literal::Str(from), Literal::Str(to)) => {
244 Ok(Literal::Str(s.replace(&**from, to)))
245 }
246 _ => Err(type_err("(string, string, string)", &args[0])),
247 },
248 BuiltinOp::Contains => match (&args[0], &args[1]) {
249 (Literal::Str(s), Literal::Str(substr)) => Ok(Literal::Bool(s.contains(&**substr))),
250 _ => Err(type_err("(string, string)", &args[0])),
251 },
252 _ => unreachable!(),
253 }
254}
255
256#[allow(clippy::cast_possible_wrap)]
258fn apply_list(op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError> {
259 match op {
260 BuiltinOp::Map | BuiltinOp::Filter | BuiltinOp::Fold | BuiltinOp::FlatMap => {
262 Err(ExprError::TypeError {
263 expected: "handled in evaluator".into(),
264 got: "direct builtin call".into(),
265 })
266 }
267 BuiltinOp::Append => match (&args[0], &args[1]) {
268 (Literal::List(items), val) => {
269 let mut new_items = items.clone();
270 new_items.push(val.clone());
271 Ok(Literal::List(new_items))
272 }
273 (other, _) => Err(type_err("list", other)),
274 },
275 BuiltinOp::Head => match &args[0] {
276 Literal::List(items) if items.is_empty() => {
277 Err(ExprError::IndexOutOfBounds { index: 0, len: 0 })
278 }
279 Literal::List(items) => Ok(items[0].clone()),
280 other => Err(type_err("list", other)),
281 },
282 BuiltinOp::Tail => match &args[0] {
283 Literal::List(items) if items.is_empty() => {
284 Err(ExprError::IndexOutOfBounds { index: 0, len: 0 })
285 }
286 Literal::List(items) => Ok(Literal::List(items[1..].to_vec())),
287 other => Err(type_err("list", other)),
288 },
289 BuiltinOp::Reverse => match &args[0] {
290 Literal::List(items) => {
291 let mut reversed = items.clone();
292 reversed.reverse();
293 Ok(Literal::List(reversed))
294 }
295 other => Err(type_err("list", other)),
296 },
297 BuiltinOp::Length => match &args[0] {
298 Literal::List(items) => Ok(Literal::Int(items.len() as i64)),
299 other => Err(type_err("list", other)),
300 },
301 _ => unreachable!(),
302 }
303}
304
305fn apply_record(op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError> {
307 match op {
308 BuiltinOp::MergeRecords => match (&args[0], &args[1]) {
309 (Literal::Record(a), Literal::Record(b)) => {
310 let mut merged = a.clone();
311 for (k, v) in b {
312 if let Some(existing) = merged.iter_mut().find(|(ek, _)| ek == k) {
313 existing.1 = v.clone();
314 } else {
315 merged.push((Arc::clone(k), v.clone()));
316 }
317 }
318 Ok(Literal::Record(merged))
319 }
320 (Literal::Record(_), other) | (other, _) => Err(type_err("record", other)),
321 },
322 BuiltinOp::Keys => match &args[0] {
323 Literal::Record(fields) => Ok(Literal::List(
324 fields
325 .iter()
326 .map(|(k, _)| Literal::Str(k.to_string()))
327 .collect(),
328 )),
329 other => Err(type_err("record", other)),
330 },
331 BuiltinOp::Values => match &args[0] {
332 Literal::Record(fields) => Ok(Literal::List(
333 fields.iter().map(|(_, v)| v.clone()).collect(),
334 )),
335 other => Err(type_err("record", other)),
336 },
337 BuiltinOp::HasField => match (&args[0], &args[1]) {
338 (Literal::Record(fields), Literal::Str(name)) => Ok(Literal::Bool(
339 fields.iter().any(|(k, _)| &**k == name.as_str()),
340 )),
341 _ => Err(type_err("(record, string)", &args[0])),
342 },
343 _ => unreachable!(),
344 }
345}
346
347#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
349fn apply_coercion(op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError> {
350 match op {
351 BuiltinOp::IntToFloat => match &args[0] {
352 Literal::Int(n) => Ok(Literal::Float(*n as f64)),
353 other => Err(type_err("int", other)),
354 },
355 BuiltinOp::FloatToInt => match &args[0] {
356 #[allow(clippy::cast_possible_truncation)]
357 Literal::Float(f) => Ok(Literal::Int(*f as i64)),
358 other => Err(type_err("float", other)),
359 },
360 BuiltinOp::IntToStr => match &args[0] {
361 Literal::Int(n) => Ok(Literal::Str(n.to_string())),
362 other => Err(type_err("int", other)),
363 },
364 BuiltinOp::FloatToStr => match &args[0] {
365 Literal::Float(f) => Ok(Literal::Str(f.to_string())),
366 other => Err(type_err("float", other)),
367 },
368 BuiltinOp::StrToInt => match &args[0] {
369 Literal::Str(s) => {
370 s.parse::<i64>()
371 .map(Literal::Int)
372 .map_err(|_| ExprError::ParseError {
373 value: s.clone(),
374 target_type: "int".into(),
375 })
376 }
377 other => Err(type_err("string", other)),
378 },
379 BuiltinOp::StrToFloat => match &args[0] {
380 Literal::Str(s) => {
381 s.parse::<f64>()
382 .map(Literal::Float)
383 .map_err(|_| ExprError::ParseError {
384 value: s.clone(),
385 target_type: "float".into(),
386 })
387 }
388 other => Err(type_err("string", other)),
389 },
390 _ => unreachable!(),
391 }
392}
393
394fn apply_inspection(op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError> {
396 match op {
397 BuiltinOp::TypeOf => Ok(Literal::Str(args[0].type_name().to_string())),
398 BuiltinOp::IsNull => Ok(Literal::Bool(args[0].is_null())),
399 BuiltinOp::IsList => Ok(Literal::Bool(matches!(args[0], Literal::List(_)))),
400 _ => unreachable!(),
401 }
402}
403
404fn numeric_binop(
406 a: &Literal,
407 b: &Literal,
408 int_op: fn(i64, i64) -> Option<i64>,
409 float_op: fn(f64, f64) -> f64,
410) -> Result<Literal, ExprError> {
411 match (a, b) {
412 (Literal::Int(x), Literal::Int(y)) => {
413 int_op(*x, *y)
414 .map(Literal::Int)
415 .ok_or_else(|| ExprError::TypeError {
416 expected: "non-overflowing arithmetic".into(),
417 got: "integer overflow".into(),
418 })
419 }
420 (Literal::Float(x), Literal::Float(y)) => Ok(Literal::Float(float_op(*x, *y))),
421 #[allow(clippy::cast_precision_loss)]
422 (Literal::Int(x), Literal::Float(y)) => Ok(Literal::Float(float_op(*x as f64, *y))),
423 #[allow(clippy::cast_precision_loss)]
424 (Literal::Float(x), Literal::Int(y)) => Ok(Literal::Float(float_op(*x, *y as f64))),
425 _ => Err(type_err("int|float", a)),
426 }
427}
428
429fn compare(
431 a: &Literal,
432 b: &Literal,
433 pred: fn(std::cmp::Ordering) -> bool,
434) -> Result<Literal, ExprError> {
435 let ord = match (a, b) {
436 (Literal::Int(x), Literal::Int(y)) => x.cmp(y),
437 (Literal::Float(x), Literal::Float(y)) => x.total_cmp(y),
438 #[allow(clippy::cast_precision_loss)]
439 (Literal::Int(x), Literal::Float(y)) => (*x as f64).total_cmp(y),
440 #[allow(clippy::cast_precision_loss)]
441 (Literal::Float(x), Literal::Int(y)) => x.total_cmp(&(*y as f64)),
442 (Literal::Str(x), Literal::Str(y)) => x.cmp(y),
443 _ => {
444 return Err(ExprError::TypeError {
445 expected: "comparable types (int, float, or string)".into(),
446 got: format!("({}, {})", a.type_name(), b.type_name()),
447 });
448 }
449 };
450 Ok(Literal::Bool(pred(ord)))
451}
452
453fn type_err(expected: &str, got: &Literal) -> ExprError {
454 ExprError::TypeError {
455 expected: expected.into(),
456 got: got.type_name().into(),
457 }
458}
459
460#[cfg(test)]
461#[allow(clippy::unwrap_used)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn add_ints() {
467 let result = apply_builtin(BuiltinOp::Add, &[Literal::Int(2), Literal::Int(3)]);
468 assert_eq!(result.unwrap(), Literal::Int(5));
469 }
470
471 #[test]
472 fn add_int_float_promotion() {
473 let result = apply_builtin(BuiltinOp::Add, &[Literal::Int(2), Literal::Float(1.5)]);
474 assert_eq!(result.unwrap(), Literal::Float(3.5));
475 }
476
477 #[test]
478 fn div_by_zero() {
479 let result = apply_builtin(BuiltinOp::Div, &[Literal::Int(1), Literal::Int(0)]);
480 assert!(matches!(result, Err(ExprError::DivisionByZero)));
481 }
482
483 #[test]
484 fn string_split_join_roundtrip() {
485 let parts = apply_builtin(
486 BuiltinOp::Split,
487 &[Literal::Str("a,b,c".into()), Literal::Str(",".into())],
488 )
489 .unwrap();
490 let joined = apply_builtin(BuiltinOp::Join, &[parts, Literal::Str(",".into())]).unwrap();
491 assert_eq!(joined, Literal::Str("a,b,c".into()));
492 }
493
494 #[test]
495 fn str_to_int_ok() {
496 let result = apply_builtin(BuiltinOp::StrToInt, &[Literal::Str("42".into())]);
497 assert_eq!(result.unwrap(), Literal::Int(42));
498 }
499
500 #[test]
501 fn str_to_int_fail() {
502 let result = apply_builtin(BuiltinOp::StrToInt, &[Literal::Str("hello".into())]);
503 assert!(matches!(result, Err(ExprError::ParseError { .. })));
504 }
505
506 #[test]
507 fn record_merge() {
508 let a = Literal::Record(vec![
509 (Arc::from("x"), Literal::Int(1)),
510 (Arc::from("y"), Literal::Int(2)),
511 ]);
512 let b = Literal::Record(vec![(Arc::from("y"), Literal::Int(99))]);
513 let result = apply_builtin(BuiltinOp::MergeRecords, &[a, b]).unwrap();
514 assert_eq!(
515 result,
516 Literal::Record(vec![
517 (Arc::from("x"), Literal::Int(1)),
518 (Arc::from("y"), Literal::Int(99)),
519 ])
520 );
521 }
522
523 #[test]
524 fn list_head_tail() {
525 let list = Literal::List(vec![Literal::Int(1), Literal::Int(2), Literal::Int(3)]);
526 assert_eq!(
527 apply_builtin(BuiltinOp::Head, std::slice::from_ref(&list)).unwrap(),
528 Literal::Int(1)
529 );
530 assert_eq!(
531 apply_builtin(BuiltinOp::Tail, &[list]).unwrap(),
532 Literal::List(vec![Literal::Int(2), Literal::Int(3)])
533 );
534 }
535
536 #[test]
537 fn empty_list_head_errors() {
538 let result = apply_builtin(BuiltinOp::Head, &[Literal::List(vec![])]);
539 assert!(matches!(result, Err(ExprError::IndexOutOfBounds { .. })));
540 }
541
542 #[test]
543 fn comparison_uses_total_cmp() {
544 let result = apply_builtin(
546 BuiltinOp::Lt,
547 &[Literal::Float(f64::NAN), Literal::Float(1.0)],
548 );
549 assert!(result.is_ok());
550 }
551}