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