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