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