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