1use tatara_lisp_eval::ffi::Arity;
36use tatara_lisp_eval::{EvalError, Interpreter, Value};
37
38fn as_str(v: &Value, span: tatara_lisp::Span) -> Result<String, EvalError> {
39 match v {
40 Value::Str(s) => Ok(s.to_string()),
41 Value::Symbol(s) | Value::Keyword(s) => Ok(s.to_string()),
44 other => Err(EvalError::type_mismatch(
45 "a string",
46 other.type_name(),
47 span,
48 )),
49 }
50}
51
52pub fn blue_equal(a: &Value, b: &Value) -> bool {
65 match (a, b) {
66 (Value::Nil, Value::Nil) => true,
67 (Value::Bool(x), Value::Bool(y)) => x == y,
68 (Value::Int(x), Value::Int(y)) => x == y,
69 (Value::Float(x), Value::Float(y)) => x == y,
70 (Value::Int(x), Value::Float(y)) | (Value::Float(y), Value::Int(x)) => (*x as f64) == *y,
71 (Value::Str(x), Value::Str(y))
72 | (Value::Symbol(x), Value::Symbol(y))
73 | (Value::Keyword(x), Value::Keyword(y)) => x == y,
74 (Value::List(x), Value::List(y)) => {
75 x.len() == y.len() && x.iter().zip(y.iter()).all(|(p, q)| blue_equal(p, q))
76 }
77 (Value::Map(x), Value::Map(y)) => {
78 x.len() == y.len()
79 && x.iter()
80 .all(|(k, v)| y.get(k).is_some_and(|w| blue_equal(v, w)))
81 }
82 (Value::Closure(x), Value::Closure(y)) => std::sync::Arc::ptr_eq(x, y),
83 (Value::NativeFn(x), Value::NativeFn(y)) => x.name == y.name,
84 (Value::Sexp(x, _), Value::Sexp(y, _)) => x == y,
85 _ => false,
86 }
87}
88
89fn render(v: &Value) -> String {
94 match v {
95 Value::Nil => String::new(),
96 Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => s.to_string(),
97 other => literal(other),
98 }
99}
100
101fn literal(v: &Value) -> String {
105 match v {
106 Value::Nil => "nil".into(),
107 Value::Bool(b) => b.to_string(),
108 Value::Int(n) => n.to_string(),
109 Value::Float(x) => float_text(*x),
110 Value::Str(s) => quoted(s),
111 Value::Symbol(s) => s.to_string(),
112 Value::Keyword(s) => format!(":{s}"),
113 Value::List(items) => {
114 let parts: Vec<String> = items.iter().map(literal).collect();
115 format!("[{}]", parts.join(", "))
116 }
117 Value::Map(m) => {
118 let mut entries: Vec<String> = m
119 .iter()
120 .map(|(k, v)| format!("{} {}", key_text(k), literal(v)))
121 .collect();
122 entries.sort();
123 format!("{{{}}}", entries.join(", "))
124 }
125 other => other.type_name().to_string(),
126 }
127}
128
129fn key_text(k: &tatara_lisp_eval::value::MapKey) -> String {
132 use tatara_lisp_eval::value::MapKey;
133 match k {
134 MapKey::Keyword(s) | MapKey::Symbol(s) => format!("{s}:"),
135 MapKey::Str(s) => format!("{} =>", quoted(s)),
136 MapKey::Int(n) => format!("{n} =>"),
137 MapKey::Float(bits) => format!("{} =>", float_text(f64::from_bits(*bits))),
138 MapKey::Bool(b) => format!("{b} =>"),
139 MapKey::Nil => "nil =>".into(),
140 }
141}
142
143fn float_text(x: f64) -> String {
145 if x.is_finite() && x.fract() == 0.0 && x.abs() < 1e16 {
146 format!("{x:.1}")
147 } else {
148 format!("{x}")
149 }
150}
151
152fn quoted(s: &str) -> String {
153 let mut out = String::with_capacity(s.len() + 2);
154 out.push('"');
155 for c in s.chars() {
156 match c {
157 '"' => out.push_str("\\\""),
158 '\\' => out.push_str("\\\\"),
159 '\n' => out.push_str("\\n"),
160 '\t' => out.push_str("\\t"),
161 c => out.push(c),
162 }
163 }
164 out.push('"');
165 out
166}
167
168fn list(items: Vec<Value>) -> Value {
169 Value::list(items)
170}
171
172pub fn install_blue_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
174 interp.register_fn(
179 "length",
180 Arity::Exact(1),
181 |a: &[Value], _h: &mut H, span| match &a[0] {
182 Value::List(items) => Ok(Value::Int(items.len() as i64)),
183 other => Ok(Value::Int(as_str(other, span)?.chars().count() as i64)),
184 },
185 );
186
187 interp.register_fn("to_s", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
188 Ok(Value::Str(render(&a[0]).into()))
189 });
190
191 interp.register_fn("upcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
192 Ok(Value::Str(as_str(&a[0], s)?.to_uppercase().into()))
193 });
194
195 interp.register_fn("downcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
196 Ok(Value::Str(as_str(&a[0], s)?.to_lowercase().into()))
197 });
198
199 interp.register_fn("trim", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
200 Ok(Value::Str(as_str(&a[0], s)?.trim().into()))
201 });
202
203 interp.register_fn(
210 "concat",
211 Arity::AtLeast(1),
212 |a: &[Value], _h: &mut H, _s| {
213 Ok(Value::Str(a.iter().map(render).collect::<String>().into()))
214 },
215 );
216
217 interp.register_fn("equal?", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
222 Ok(Value::Bool(blue_equal(&a[0], &a[1])))
223 });
224 interp.register_fn("not=", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
225 Ok(Value::Bool(!blue_equal(&a[0], &a[1])))
226 });
227
228 interp.register_fn("nil?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
234 Ok(Value::Bool(matches!(a[0], Value::Nil)))
235 });
236 interp.register_fn("list?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
237 Ok(Value::Bool(matches!(a[0], Value::List(_))))
238 });
239 interp.register_fn("map?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
240 Ok(Value::Bool(matches!(a[0], Value::Map(_))))
241 });
242 interp.register_fn("float?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
243 Ok(Value::Bool(matches!(a[0], Value::Float(_))))
244 });
245 interp.register_fn("bool?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
246 Ok(Value::Bool(matches!(a[0], Value::Bool(_))))
247 });
248
249 interp.register_fn("cdr", Arity::Exact(1), |a: &[Value], _h: &mut H, span| {
254 match &a[0] {
255 Value::List(xs) if !xs.is_empty() => Ok(Value::List(std::sync::Arc::new(xs.skip(1)))),
257 Value::Nil | Value::List(_) => Err(EvalError::native_fn(
258 std::sync::Arc::<str>::from("cdr"),
259 "cdr of empty list",
260 span,
261 )),
262 other => Err(EvalError::type_mismatch("pair", other.type_name(), span)),
263 }
264 });
265 interp.register_fn("append", Arity::Any, |a: &[Value], _h: &mut H, span| {
266 let mut out = tatara_lisp_eval::List::new();
270 for v in a {
271 match v {
272 Value::Nil => {}
273 Value::List(xs) => out.append(tatara_lisp_eval::List::clone(xs)),
274 other => return Err(EvalError::type_mismatch("list", other.type_name(), span)),
275 }
276 }
277 Ok(Value::List(std::sync::Arc::new(out)))
278 });
279
280 interp.register_fn("split", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
281 let text = as_str(&a[0], s)?;
282 let sep = as_str(&a[1], s)?;
283 let parts: Vec<Value> = if sep.is_empty() {
292 text.chars()
293 .map(|c| Value::Str(c.to_string().into()))
294 .collect()
295 } else {
296 text.split(sep.as_str())
297 .map(|p| Value::Str(p.into()))
298 .collect()
299 };
300 Ok(list(parts))
301 });
302
303 interp.register_fn("join", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
304 let sep = as_str(&a[1], s)?;
305 match &a[0] {
306 Value::List(items) => Ok(Value::Str(
307 items
308 .iter()
309 .map(render)
310 .collect::<Vec<_>>()
311 .join(&sep)
312 .into(),
313 )),
314 other => Err(EvalError::type_mismatch("a list", other.type_name(), s).into()),
315 }
316 });
317
318 interp.register_fn(
319 "contains?",
320 Arity::Exact(2),
321 |a: &[Value], _h: &mut H, s| {
322 Ok(Value::Bool(as_str(&a[0], s)?.contains(&as_str(&a[1], s)?)))
323 },
324 );
325
326 interp.register_fn(
327 "starts_with?",
328 Arity::Exact(2),
329 |a: &[Value], _h: &mut H, s| {
330 Ok(Value::Bool(
331 as_str(&a[0], s)?.starts_with(&as_str(&a[1], s)?),
332 ))
333 },
334 );
335
336 interp.register_fn(
337 "ends_with?",
338 Arity::Exact(2),
339 |a: &[Value], _h: &mut H, s| {
340 Ok(Value::Bool(as_str(&a[0], s)?.ends_with(&as_str(&a[1], s)?)))
341 },
342 );
343
344 interp.register_fn("replace", Arity::Exact(3), |a: &[Value], _h: &mut H, s| {
345 Ok(Value::Str(
346 as_str(&a[0], s)?
347 .replace(&as_str(&a[1], s)?, &as_str(&a[2], s)?)
348 .into(),
349 ))
350 });
351
352 interp.register_fn("reverse", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
353 match &a[0] {
354 Value::List(items) => Ok(Value::list(items.iter().rev().cloned())),
355 other => Ok(Value::Str(
358 as_str(other, s)?.chars().rev().collect::<String>().into(),
359 )),
360 }
361 });
362
363 interp.register_fn("chars", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
364 Ok(list(
365 as_str(&a[0], s)?
366 .chars()
367 .map(|c| Value::Str(c.to_string().into()))
368 .collect(),
369 ))
370 });
371
372 interp.register_fn(
379 "to_int",
380 Arity::Exact(1),
381 |a: &[Value], _h: &mut H, s| match &a[0] {
382 Value::Int(n) => Ok(Value::Int(*n)),
383 Value::Float(x) => Ok(float_to_int(*x).map_or(Value::Nil, Value::Int)),
387 other => Ok(as_str(other, s)?
388 .trim()
389 .parse::<i64>()
390 .map_or(Value::Nil, Value::Int)),
391 },
392 );
393
394 interp.register_fn(
395 "to_int!",
396 Arity::Exact(1),
397 |a: &[Value], _h: &mut H, s| match &a[0] {
398 Value::Int(n) => Ok(Value::Int(*n)),
399 Value::Float(x) => float_to_int(*x)
400 .map(Value::Int)
401 .ok_or(EvalError::IntegerOverflow {
402 op: "to_int!",
403 at: s,
404 }),
405 other => {
406 let text = as_str(other, s)?;
407 text.trim().parse::<i64>().map(Value::Int).map_err(|_| {
408 EvalError::native_fn(
409 "to_int!",
410 "`".to_string() + &text + "` is not an integer",
411 s,
412 )
413 .into()
414 })
415 }
416 },
417 );
418
419 interp.register_fn(
420 "to_float",
421 Arity::Exact(1),
422 |a: &[Value], _h: &mut H, s| match &a[0] {
423 Value::Float(x) => Ok(Value::Float(*x)),
424 Value::Int(n) => Ok(Value::Float(*n as f64)),
425 other => Ok(as_str(other, s)?
426 .trim()
427 .parse::<f64>()
428 .map_or(Value::Nil, Value::Float)),
429 },
430 );
431
432 interp.register_fn(
433 "abs",
434 Arity::Exact(1),
435 |a: &[Value], _h: &mut H, s| match &a[0] {
436 Value::Int(n) => n
439 .checked_abs()
440 .map(Value::Int)
441 .ok_or(EvalError::IntegerOverflow { op: "abs", at: s }),
442 Value::Float(x) => Ok(Value::Float(x.abs())),
443 other => Err(EvalError::type_mismatch("a number", other.type_name(), s).into()),
444 },
445 );
446
447 interp.register_fn("range", Arity::Range(1, 3), |a: &[Value], _h: &mut H, s| {
468 let (start, end, step) = match a.len() {
469 1 => (Value::Int(0), a[0].clone(), Value::Int(1)),
470 2 => (a[0].clone(), a[1].clone(), Value::Int(1)),
471 _ => (a[0].clone(), a[1].clone(), a[2].clone()),
472 };
473 range_list(start, &end, &step, s)
474 });
475
476 interp.register_fn(
483 "write_stdout",
484 Arity::Exact(1),
485 |a: &[Value], _h: &mut H, s| {
486 use std::io::Write;
487 let text = as_str(&a[0], s)?;
488 let mut out = std::io::stdout().lock();
489 out.write_all(text.as_bytes())
490 .and_then(|()| out.flush())
491 .map_err(|e| EvalError::native_fn("write_stdout", e.to_string(), s))?;
492 Ok(Value::Nil)
493 },
494 );
495 interp.register_fn(
496 "write_stderr",
497 Arity::Exact(1),
498 |a: &[Value], _h: &mut H, s| {
499 use std::io::Write;
500 let text = as_str(&a[0], s)?;
501 let mut err = std::io::stderr().lock();
502 err.write_all(text.as_bytes())
503 .and_then(|()| err.flush())
504 .map_err(|e| EvalError::native_fn("write_stderr", e.to_string(), s))?;
505 Ok(Value::Nil)
506 },
507 );
508
509 interp.register_higher_order_fn(
516 "sort_keyed",
517 Arity::Exact(2),
518 |a: &[Value], host: &mut H, caller: &tatara_lisp_eval::ffi::Caller<H>, s| {
519 let xs = match &a[1] {
520 Value::List(xs) => xs.to_vec(),
521 Value::Nil => Vec::new(),
522 other => {
523 return Err(EvalError::type_mismatch("a list", other.type_name(), s));
524 }
525 };
526 let mut keyed = Vec::with_capacity(xs.len());
527 for x in xs {
528 keyed.push((caller.apply_value(&a[0], vec![x.clone()], host, s)?, x));
529 }
530 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
531 },
532 );
533}
534
535fn range_num(v: &Value, s: tatara_lisp::Span) -> Result<f64, EvalError> {
537 match v {
538 Value::Int(n) => Ok(*n as f64),
539 Value::Float(x) => Ok(*x),
540 other => Err(EvalError::native_fn(
541 "range",
542 "expected a number, got ".to_string() + other.type_name(),
543 s,
544 )),
545 }
546}
547
548fn float_to_int(x: f64) -> Option<i64> {
551 const LIMIT: f64 = 9_223_372_036_854_775_808.0;
552 let t = x.trunc();
553 #[allow(clippy::cast_possible_truncation)]
554 (t.is_finite() && (-LIMIT..LIMIT).contains(&t)).then(|| t as i64)
555}
556
557fn range_add(a: &Value, b: &Value, s: tatara_lisp::Span) -> Result<Value, EvalError> {
560 match (a, b) {
561 (Value::Int(x), Value::Int(y)) => x.checked_add(*y).map(Value::Int).ok_or_else(|| {
562 EvalError::native_fn("range", "an element overflowed a 64-bit integer", s)
563 }),
564 _ => Ok(Value::Float(range_num(a, s)? + range_num(b, s)?)),
565 }
566}
567
568fn range_list(
569 start: Value,
570 end: &Value,
571 step: &Value,
572 s: tatara_lisp::Span,
573) -> Result<Value, EvalError> {
574 let end_f = range_num(end, s)?;
575 let step_f = range_num(step, s)?;
576 range_num(&start, s)?;
577 if step_f == 0.0 {
578 return Err(EvalError::native_fn(
579 "range",
580 "a step of zero never reaches the end",
581 s,
582 ));
583 }
584 let mut out = Vec::new();
585 let mut cur = start;
586 loop {
587 let c = range_num(&cur, s)?;
588 if (step_f > 0.0 && c >= end_f) || (step_f < 0.0 && c <= end_f) {
589 break;
590 }
591 let next = range_add(&cur, step, s)?;
592 out.push(cur);
593 cur = next;
594 }
595 Ok(list(out))
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601
602 #[test]
603 fn sort_keyed_is_stable_keyed_and_deep() {
604 assert_eq!(
606 ints("map(fn(p) last(p) end, sort_keyed(fn(p) first(p) end, [[2, 1], [1, 2], [2, 3], [1, 4]]))"),
607 vec![2, 4, 1, 3]
608 );
609 assert_eq!(
611 i("first(sort_keyed(fn(x) x end, reverse(range(0, 20000))))"),
612 0
613 );
614 assert_eq!(i("last(sort_keyed(fn(x) 0 - x end, range(0, 20000)))"), 0);
615 assert_eq!(ints("sort_keyed(fn(x) x end, [])"), Vec::<i64>::new());
617 assert!(crate::run("sort_keyed(fn(x) x end, [1, \"a\"])").is_err());
618 }
619
620 fn eval(src: &str) -> Value {
621 crate::run(src)
622 .unwrap_or_else(|e| panic!("{src:?}: {e}"))
623 .value
624 }
625
626 fn s(src: &str) -> String {
627 match eval(src) {
628 Value::Str(v) => v.to_string(),
629 other => panic!("{src:?} produced {other:?}"),
630 }
631 }
632
633 fn i(src: &str) -> i64 {
634 match eval(src) {
635 Value::Int(v) => v,
636 other => panic!("{src:?} produced {other:?}"),
637 }
638 }
639
640 fn ints(src: &str) -> Vec<i64> {
641 match eval(src) {
642 Value::List(xs) => xs
643 .iter()
644 .map(|v| match v {
645 Value::Int(n) => *n,
646 other => panic!("{src:?} produced a non-Int element {other:?}"),
647 })
648 .collect(),
649 other => panic!("{src:?} produced {other:?}"),
650 }
651 }
652
653 #[test]
656 fn range_answers_as_the_lisp_definition_did() {
657 assert_eq!(ints("range(5)"), vec![0, 1, 2, 3, 4]);
658 assert_eq!(ints("range(2, 6)"), vec![2, 3, 4, 5]);
659 assert_eq!(ints("range(0, 10, 2)"), vec![0, 2, 4, 6, 8]);
660 assert_eq!(ints("range(10, 0, 0 - 2)"), vec![10, 8, 6, 4, 2]);
661 assert!(ints("range(5, 5)").is_empty());
662 assert!(ints("range(5, 2)").is_empty());
663 assert!(ints("range(0)").is_empty());
664 match eval("range(0, 1, 0.5)") {
666 Value::List(xs) => {
667 assert!(matches!(xs[0], Value::Int(0)), "{xs:?}");
668 assert!(matches!(xs[1], Value::Float(x) if x == 0.5), "{xs:?}");
669 assert_eq!(xs.len(), 2);
670 }
671 other => panic!("{other:?}"),
672 }
673 }
674
675 #[test]
681 fn range_builds_long_lists_without_recursing() {
682 assert_eq!(i("length(range(0, 200000))"), 200_000);
683 assert_eq!(i("nth(199999, range(0, 200000))"), 199_999);
684 }
685
686 #[test]
689 fn range_refuses_a_zero_step_and_a_non_number() {
690 assert!(crate::run("range(0, 5, 0)").is_err());
691 assert!(crate::run("range(0, \"x\")").is_err());
692 }
693
694 #[test]
698 fn length_counts_characters_not_bytes() {
699 assert_eq!(i("length(\"hello\")"), 5);
700 assert_eq!(i("length(\"héllo\")"), 5, "must not be 6");
702 assert_eq!(i("length(\"😀\")"), 1, "must not be 4");
704 }
705
706 #[test]
709 fn a_combining_sequence_counts_scalars_not_graphemes() {
710 assert_eq!(
712 i("length(\"e\\u{301}\")"),
713 2,
714 "blue counts scalar values; Elixir's String.length would say 1"
715 );
716 }
717
718 #[test]
719 fn length_also_works_on_a_list() {
720 assert_eq!(i("length([1, 2, 3])"), 3);
721 }
722
723 #[test]
724 fn case_and_trim() {
725 assert_eq!(s("upcase(\"abc\")"), "ABC");
726 assert_eq!(s("downcase(\"ABC\")"), "abc");
727 assert_eq!(s("trim(\" hi \")"), "hi");
728 assert_eq!(s("upcase(\"é\")"), "É");
730 }
731
732 #[test]
733 fn concat_and_to_s() {
734 assert_eq!(s("concat(\"a\", \"b\")"), "ab");
735 assert_eq!(s("concat(\"n=\", 42)"), "n=42");
736 assert_eq!(s("to_s(42)"), "42");
737 assert_eq!(s("to_s(true)"), "true");
738 }
739
740 #[test]
744 fn plus_is_not_string_concatenation() {
745 assert!(
746 crate::run("\"a\" + \"b\"").is_err(),
747 "`+` must not silently concatenate — use concat"
748 );
749 }
750
751 #[test]
752 fn split_and_join() {
753 assert_eq!(i("length(split(\"a,b,c\", \",\"))"), 3);
754 assert_eq!(s("join(split(\"a,b,c\", \",\"), \"-\")"), "a-b-c");
755 assert_eq!(i("length(split(\"abc\", \"\"))"), 3);
757 }
758
759 #[test]
760 fn predicates() {
761 assert!(matches!(
762 eval("contains?(\"hello\", \"ell\")"),
763 Value::Bool(true)
764 ));
765 assert!(matches!(
766 eval("contains?(\"hello\", \"xyz\")"),
767 Value::Bool(false)
768 ));
769 assert!(matches!(
770 eval("starts_with?(\"hello\", \"he\")"),
771 Value::Bool(true)
772 ));
773 assert!(matches!(
774 eval("ends_with?(\"hello\", \"lo\")"),
775 Value::Bool(true)
776 ));
777 }
778
779 #[test]
780 fn replace_and_chars() {
781 assert_eq!(s("replace(\"a-b-c\", \"-\", \"+\")"), "a+b+c");
782 assert_eq!(i("length(chars(\"abc\"))"), 3);
783 }
784
785 #[test]
788 fn reverse_is_character_wise() {
789 assert_eq!(s("reverse(\"abc\")"), "cba");
790 assert_eq!(s("reverse(\"héllo\")"), "olléh", "must not corrupt the é");
791 }
792
793 #[test]
794 fn reverse_also_works_on_a_list() {
795 assert_eq!(s("join(reverse([1, 2, 3]), \",\")"), "3,2,1");
796 }
797
798 #[test]
803 fn to_int_is_nil_on_garbage_rather_than_zero() {
804 assert_eq!(i("to_int(\"42\")"), 42);
805 assert!(
806 matches!(eval("to_int(\"banana\")"), Value::Nil),
807 "Ruby would say 0 here; a falsy nil cannot be mistaken for a result"
808 );
809 assert!(
810 matches!(eval("to_int(\"0\")"), Value::Int(0)),
811 "and a real 0 is still a real 0 — the two must stay distinguishable"
812 );
813 }
814
815 #[test]
816 fn to_int_bang_raises_on_garbage() {
817 assert_eq!(i("to_int!(\"42\")"), 42);
818 let err = crate::run("to_int!(\"banana\")").expect_err("must raise");
819 assert!(err.to_string().contains("banana"), "must name it: {err}");
820 }
821
822 #[test]
823 fn numeric_conversions_and_abs() {
824 assert_eq!(i("to_int(3.9)"), 3);
825 assert_eq!(i("abs(0 - 5)"), 5);
826 assert!(matches!(eval("to_float(\"1.5\")"), Value::Float(_)));
827 assert!(matches!(eval("to_float(\"nope\")"), Value::Nil));
828 }
829
830 #[test]
832 fn a_non_string_argument_is_a_type_error() {
833 assert!(crate::run("upcase([1, 2])").is_err());
834 assert!(crate::run("join(\"not a list\", \",\")").is_err());
835 }
836}