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(
190 "builtin_names",
191 Arity::Exact(0),
192 |_a: &[Value], _h: &mut H, _s| {
193 Ok(Value::list(
194 crate::docs::NAMES
195 .iter()
196 .map(|n| Value::Str(std::sync::Arc::from(n.name))),
197 ))
198 },
199 );
200 interp.register_fn("to_s", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
201 Ok(Value::Str(render(&a[0]).into()))
202 });
203
204 interp.register_fn("upcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
205 Ok(Value::Str(as_str(&a[0], s)?.to_uppercase().into()))
206 });
207
208 interp.register_fn("downcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
209 Ok(Value::Str(as_str(&a[0], s)?.to_lowercase().into()))
210 });
211
212 interp.register_fn("trim", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
213 Ok(Value::Str(as_str(&a[0], s)?.trim().into()))
214 });
215
216 interp.register_fn(
223 "concat",
224 Arity::AtLeast(1),
225 |a: &[Value], _h: &mut H, _s| {
226 Ok(Value::Str(a.iter().map(render).collect::<String>().into()))
227 },
228 );
229
230 interp.register_fn("equal?", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
235 Ok(Value::Bool(blue_equal(&a[0], &a[1])))
236 });
237 interp.register_fn("not=", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
238 Ok(Value::Bool(!blue_equal(&a[0], &a[1])))
239 });
240
241 interp.register_fn("nil?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
247 Ok(Value::Bool(matches!(a[0], Value::Nil)))
248 });
249 interp.register_fn("list?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
250 Ok(Value::Bool(matches!(a[0], Value::List(_))))
251 });
252 interp.register_fn("map?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
253 Ok(Value::Bool(matches!(a[0], Value::Map(_))))
254 });
255 interp.register_fn("float?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
256 Ok(Value::Bool(matches!(a[0], Value::Float(_))))
257 });
258 interp.register_fn("bool?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
259 Ok(Value::Bool(matches!(a[0], Value::Bool(_))))
260 });
261
262 interp.register_fn("cdr", Arity::Exact(1), |a: &[Value], _h: &mut H, span| {
267 match &a[0] {
268 Value::List(xs) if !xs.is_empty() => Ok(Value::List(std::sync::Arc::new(xs.skip(1)))),
270 Value::Nil | Value::List(_) => Err(EvalError::native_fn(
271 std::sync::Arc::<str>::from("cdr"),
272 "cdr of empty list",
273 span,
274 )),
275 other => Err(EvalError::type_mismatch("pair", other.type_name(), span)),
276 }
277 });
278 interp.register_owned_fn("append", Arity::Any, |a: Vec<Value>, _h: &mut H, span| {
279 let mut lists = Vec::with_capacity(a.len());
286 for v in a {
287 match v {
288 Value::Nil => {}
289 Value::List(xs) => lists.push(xs),
290 other => return Err(EvalError::type_mismatch("list", other.type_name(), span)),
291 }
292 }
293 let mut lists = lists.into_iter();
294 let Some(mut out) = lists.next() else {
295 return Ok(Value::List(std::sync::Arc::new(
296 tatara_lisp_eval::List::new(),
297 )));
298 };
299 let acc = std::sync::Arc::make_mut(&mut out);
300 for xs in lists {
301 acc.append(
302 std::sync::Arc::try_unwrap(xs)
303 .unwrap_or_else(|shared| tatara_lisp_eval::List::clone(&shared)),
304 );
305 }
306 Ok(Value::List(out))
307 });
308
309 interp.register_fn("split", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
310 let text = as_str(&a[0], s)?;
311 let sep = as_str(&a[1], s)?;
312 let parts: Vec<Value> = if sep.is_empty() {
321 text.chars()
322 .map(|c| Value::Str(c.to_string().into()))
323 .collect()
324 } else {
325 text.split(sep.as_str())
326 .map(|p| Value::Str(p.into()))
327 .collect()
328 };
329 Ok(list(parts))
330 });
331
332 interp.register_fn("join", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
333 let sep = as_str(&a[1], s)?;
334 match &a[0] {
335 Value::List(items) => Ok(Value::Str(
336 items
337 .iter()
338 .map(render)
339 .collect::<Vec<_>>()
340 .join(&sep)
341 .into(),
342 )),
343 other => Err(EvalError::type_mismatch("a list", other.type_name(), s).into()),
344 }
345 });
346
347 interp.register_fn(
348 "contains?",
349 Arity::Exact(2),
350 |a: &[Value], _h: &mut H, s| {
351 Ok(Value::Bool(as_str(&a[0], s)?.contains(&as_str(&a[1], s)?)))
352 },
353 );
354
355 interp.register_fn(
356 "starts_with?",
357 Arity::Exact(2),
358 |a: &[Value], _h: &mut H, s| {
359 Ok(Value::Bool(
360 as_str(&a[0], s)?.starts_with(&as_str(&a[1], s)?),
361 ))
362 },
363 );
364
365 interp.register_fn(
366 "ends_with?",
367 Arity::Exact(2),
368 |a: &[Value], _h: &mut H, s| {
369 Ok(Value::Bool(as_str(&a[0], s)?.ends_with(&as_str(&a[1], s)?)))
370 },
371 );
372
373 interp.register_fn(
374 "text_index",
375 Arity::Exact(3),
376 |a: &[Value], _h: &mut H, s| {
377 Ok(Value::Int(text_index(
378 &as_str(&a[0], s)?,
379 &as_str(&a[1], s)?,
380 as_index(&a[2], s)?,
381 )))
382 },
383 );
384
385 interp.register_fn(
386 "text_last_index",
387 Arity::Exact(3),
388 |a: &[Value], _h: &mut H, s| {
389 Ok(Value::Int(text_last_index(
390 &as_str(&a[0], s)?,
391 &as_str(&a[1], s)?,
392 as_index(&a[2], s)?,
393 )))
394 },
395 );
396
397 interp.register_fn("replace", Arity::Exact(3), |a: &[Value], _h: &mut H, s| {
398 Ok(Value::Str(
399 as_str(&a[0], s)?
400 .replace(&as_str(&a[1], s)?, &as_str(&a[2], s)?)
401 .into(),
402 ))
403 });
404
405 interp.register_fn("reverse", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
406 match &a[0] {
407 Value::List(items) => Ok(Value::list(items.iter().rev().cloned())),
408 other => Ok(Value::Str(
411 as_str(other, s)?.chars().rev().collect::<String>().into(),
412 )),
413 }
414 });
415
416 interp.register_fn("chars", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
417 Ok(list(
418 as_str(&a[0], s)?
419 .chars()
420 .map(|c| Value::Str(c.to_string().into()))
421 .collect(),
422 ))
423 });
424
425 interp.register_fn(
432 "to_int",
433 Arity::Exact(1),
434 |a: &[Value], _h: &mut H, s| match &a[0] {
435 Value::Int(n) => Ok(Value::Int(*n)),
436 Value::Float(x) => Ok(float_to_int(*x).map_or(Value::Nil, Value::Int)),
440 other => Ok(as_str(other, s)?
441 .trim()
442 .parse::<i64>()
443 .map_or(Value::Nil, Value::Int)),
444 },
445 );
446
447 interp.register_fn(
448 "to_int!",
449 Arity::Exact(1),
450 |a: &[Value], _h: &mut H, s| match &a[0] {
451 Value::Int(n) => Ok(Value::Int(*n)),
452 Value::Float(x) => float_to_int(*x)
453 .map(Value::Int)
454 .ok_or(EvalError::IntegerOverflow {
455 op: "to_int!",
456 at: s,
457 }),
458 other => {
459 let text = as_str(other, s)?;
460 text.trim().parse::<i64>().map(Value::Int).map_err(|_| {
461 EvalError::native_fn(
462 "to_int!",
463 "`".to_string() + &text + "` is not an integer",
464 s,
465 )
466 .into()
467 })
468 }
469 },
470 );
471
472 interp.register_fn(
473 "to_float",
474 Arity::Exact(1),
475 |a: &[Value], _h: &mut H, s| match &a[0] {
476 Value::Float(x) => Ok(Value::Float(*x)),
477 Value::Int(n) => Ok(Value::Float(*n as f64)),
478 other => Ok(as_str(other, s)?
479 .trim()
480 .parse::<f64>()
481 .map_or(Value::Nil, Value::Float)),
482 },
483 );
484
485 interp.register_fn(
486 "abs",
487 Arity::Exact(1),
488 |a: &[Value], _h: &mut H, s| match &a[0] {
489 Value::Int(n) => n
492 .checked_abs()
493 .map(Value::Int)
494 .ok_or(EvalError::IntegerOverflow { op: "abs", at: s }),
495 Value::Float(x) => Ok(Value::Float(x.abs())),
496 other => Err(EvalError::type_mismatch("a number", other.type_name(), s).into()),
497 },
498 );
499
500 interp.register_fn("range", Arity::Range(1, 3), |a: &[Value], _h: &mut H, s| {
521 let (start, end, step) = match a.len() {
522 1 => (Value::Int(0), a[0].clone(), Value::Int(1)),
523 2 => (a[0].clone(), a[1].clone(), Value::Int(1)),
524 _ => (a[0].clone(), a[1].clone(), a[2].clone()),
525 };
526 range_list(start, &end, &step, s)
527 });
528
529 interp.register_fn(
536 "write_stdout",
537 Arity::Exact(1),
538 |a: &[Value], _h: &mut H, s| {
539 use std::io::Write;
540 let text = as_str(&a[0], s)?;
541 let mut out = std::io::stdout().lock();
542 out.write_all(text.as_bytes())
543 .and_then(|()| out.flush())
544 .map_err(|e| EvalError::native_fn("write_stdout", e.to_string(), s))?;
545 Ok(Value::Nil)
546 },
547 );
548 interp.register_fn(
549 "write_stderr",
550 Arity::Exact(1),
551 |a: &[Value], _h: &mut H, s| {
552 use std::io::Write;
553 let text = as_str(&a[0], s)?;
554 let mut err = std::io::stderr().lock();
555 err.write_all(text.as_bytes())
556 .and_then(|()| err.flush())
557 .map_err(|e| EvalError::native_fn("write_stderr", e.to_string(), s))?;
558 Ok(Value::Nil)
559 },
560 );
561
562 interp.register_higher_order_fn(
569 "sort_keyed",
570 Arity::Exact(2),
571 |a: &[Value], host: &mut H, caller: &tatara_lisp_eval::ffi::Caller<H>, s| {
572 let xs = match &a[1] {
573 Value::List(xs) => xs.to_vec(),
574 Value::Nil => Vec::new(),
575 other => {
576 return Err(EvalError::type_mismatch("a list", other.type_name(), s));
577 }
578 };
579 let mut keyed = Vec::with_capacity(xs.len());
580 for x in xs {
581 keyed.push((caller.apply_value(&a[0], vec![x.clone()], host, s)?, x));
582 }
583 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
584 },
585 );
586}
587
588fn as_index(v: &Value, span: tatara_lisp::Span) -> Result<i64, EvalError> {
589 match v {
590 Value::Int(n) => Ok(*n),
591 other => Err(EvalError::type_mismatch(
592 "an integer",
593 other.type_name(),
594 span,
595 )),
596 }
597}
598
599fn text_index(text: &str, sub: &str, from: i64) -> i64 {
600 let from = usize::try_from(from.max(0)).unwrap_or(usize::MAX);
601 let start = match text.char_indices().nth(from) {
602 Some((b, _)) => b,
603 None if text.chars().count() == from => text.len(),
604 None => return -1,
605 };
606 text[start..].find(sub).map_or(-1, |b| {
607 i64::try_from(from + text[start..start + b].chars().count()).unwrap_or(i64::MAX)
608 })
609}
610
611fn text_last_index(text: &str, sub: &str, from: i64) -> i64 {
612 let Ok(from) = usize::try_from(from) else {
613 return -1;
614 };
615 let total = text.chars().count();
616 let width = sub.chars().count();
617 if width > total {
618 return -1;
619 }
620 let limit = from.min(total - width);
621 let end = text
622 .char_indices()
623 .nth(limit + width)
624 .map_or(text.len(), |(b, _)| b);
625 text[..end].rfind(sub).map_or(-1, |b| {
626 i64::try_from(text[..b].chars().count()).unwrap_or(i64::MAX)
627 })
628}
629
630fn range_num(v: &Value, s: tatara_lisp::Span) -> Result<f64, EvalError> {
632 match v {
633 Value::Int(n) => Ok(*n as f64),
634 Value::Float(x) => Ok(*x),
635 other => Err(EvalError::native_fn(
636 "range",
637 "expected a number, got ".to_string() + other.type_name(),
638 s,
639 )),
640 }
641}
642
643fn float_to_int(x: f64) -> Option<i64> {
646 const LIMIT: f64 = 9_223_372_036_854_775_808.0;
647 let t = x.trunc();
648 #[allow(clippy::cast_possible_truncation)]
649 (t.is_finite() && (-LIMIT..LIMIT).contains(&t)).then(|| t as i64)
650}
651
652fn range_add(a: &Value, b: &Value, s: tatara_lisp::Span) -> Result<Value, EvalError> {
655 match (a, b) {
656 (Value::Int(x), Value::Int(y)) => x.checked_add(*y).map(Value::Int).ok_or_else(|| {
657 EvalError::native_fn("range", "an element overflowed a 64-bit integer", s)
658 }),
659 _ => Ok(Value::Float(range_num(a, s)? + range_num(b, s)?)),
660 }
661}
662
663fn range_list(
664 start: Value,
665 end: &Value,
666 step: &Value,
667 s: tatara_lisp::Span,
668) -> Result<Value, EvalError> {
669 let end_f = range_num(end, s)?;
670 let step_f = range_num(step, s)?;
671 range_num(&start, s)?;
672 if step_f == 0.0 {
673 return Err(EvalError::native_fn(
674 "range",
675 "a step of zero never reaches the end",
676 s,
677 ));
678 }
679 let mut out = Vec::new();
680 let mut cur = start;
681 loop {
682 let c = range_num(&cur, s)?;
683 if (step_f > 0.0 && c >= end_f) || (step_f < 0.0 && c <= end_f) {
684 break;
685 }
686 let next = range_add(&cur, step, s)?;
687 out.push(cur);
688 cur = next;
689 }
690 Ok(list(out))
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696
697 #[test]
698 fn sort_keyed_is_stable_keyed_and_deep() {
699 assert_eq!(
701 ints("map(fn(p) last(p) end, sort_keyed(fn(p) first(p) end, [[2, 1], [1, 2], [2, 3], [1, 4]]))"),
702 vec![2, 4, 1, 3]
703 );
704 assert_eq!(
706 i("first(sort_keyed(fn(x) x end, reverse(range(0, 20000))))"),
707 0
708 );
709 assert_eq!(i("last(sort_keyed(fn(x) 0 - x end, range(0, 20000)))"), 0);
710 assert_eq!(ints("sort_keyed(fn(x) x end, [])"), Vec::<i64>::new());
712 assert!(crate::run("sort_keyed(fn(x) x end, [1, \"a\"])").is_err());
713 }
714
715 fn eval(src: &str) -> Value {
716 crate::run(src)
717 .unwrap_or_else(|e| panic!("{src:?}: {e}"))
718 .value
719 }
720
721 fn s(src: &str) -> String {
722 match eval(src) {
723 Value::Str(v) => v.to_string(),
724 other => panic!("{src:?} produced {other:?}"),
725 }
726 }
727
728 fn i(src: &str) -> i64 {
729 match eval(src) {
730 Value::Int(v) => v,
731 other => panic!("{src:?} produced {other:?}"),
732 }
733 }
734
735 fn ints(src: &str) -> Vec<i64> {
736 match eval(src) {
737 Value::List(xs) => xs
738 .iter()
739 .map(|v| match v {
740 Value::Int(n) => *n,
741 other => panic!("{src:?} produced a non-Int element {other:?}"),
742 })
743 .collect(),
744 other => panic!("{src:?} produced {other:?}"),
745 }
746 }
747
748 #[test]
751 fn range_answers_as_the_lisp_definition_did() {
752 assert_eq!(ints("range(5)"), vec![0, 1, 2, 3, 4]);
753 assert_eq!(ints("range(2, 6)"), vec![2, 3, 4, 5]);
754 assert_eq!(ints("range(0, 10, 2)"), vec![0, 2, 4, 6, 8]);
755 assert_eq!(ints("range(10, 0, 0 - 2)"), vec![10, 8, 6, 4, 2]);
756 assert!(ints("range(5, 5)").is_empty());
757 assert!(ints("range(5, 2)").is_empty());
758 assert!(ints("range(0)").is_empty());
759 match eval("range(0, 1, 0.5)") {
761 Value::List(xs) => {
762 assert!(matches!(xs[0], Value::Int(0)), "{xs:?}");
763 assert!(matches!(xs[1], Value::Float(x) if x == 0.5), "{xs:?}");
764 assert_eq!(xs.len(), 2);
765 }
766 other => panic!("{other:?}"),
767 }
768 }
769
770 #[test]
776 fn range_builds_long_lists_without_recursing() {
777 assert_eq!(i("length(range(0, 200000))"), 200_000);
778 assert_eq!(i("nth(199999, range(0, 200000))"), 199_999);
779 }
780
781 #[test]
784 fn range_refuses_a_zero_step_and_a_non_number() {
785 assert!(crate::run("range(0, 5, 0)").is_err());
786 assert!(crate::run("range(0, \"x\")").is_err());
787 }
788
789 #[test]
793 fn length_counts_characters_not_bytes() {
794 assert_eq!(i("length(\"hello\")"), 5);
795 assert_eq!(i("length(\"héllo\")"), 5, "must not be 6");
797 assert_eq!(i("length(\"😀\")"), 1, "must not be 4");
799 }
800
801 #[test]
804 fn a_combining_sequence_counts_scalars_not_graphemes() {
805 assert_eq!(
807 i("length(\"e\\u{301}\")"),
808 2,
809 "blue counts scalar values; Elixir's String.length would say 1"
810 );
811 }
812
813 #[test]
814 fn length_also_works_on_a_list() {
815 assert_eq!(i("length([1, 2, 3])"), 3);
816 }
817
818 #[test]
819 fn case_and_trim() {
820 assert_eq!(s("upcase(\"abc\")"), "ABC");
821 assert_eq!(s("downcase(\"ABC\")"), "abc");
822 assert_eq!(s("trim(\" hi \")"), "hi");
823 assert_eq!(s("upcase(\"é\")"), "É");
825 }
826
827 #[test]
828 fn concat_and_to_s() {
829 assert_eq!(s("concat(\"a\", \"b\")"), "ab");
830 assert_eq!(s("concat(\"n=\", 42)"), "n=42");
831 assert_eq!(s("to_s(42)"), "42");
832 assert_eq!(s("to_s(true)"), "true");
833 }
834
835 #[test]
839 fn plus_is_not_string_concatenation() {
840 assert!(
841 crate::run("\"a\" + \"b\"").is_err(),
842 "`+` must not silently concatenate — use concat"
843 );
844 }
845
846 #[test]
847 fn split_and_join() {
848 assert_eq!(i("length(split(\"a,b,c\", \",\"))"), 3);
849 assert_eq!(s("join(split(\"a,b,c\", \",\"), \"-\")"), "a-b-c");
850 assert_eq!(i("length(split(\"abc\", \"\"))"), 3);
852 }
853
854 #[test]
855 fn predicates() {
856 assert!(matches!(
857 eval("contains?(\"hello\", \"ell\")"),
858 Value::Bool(true)
859 ));
860 assert!(matches!(
861 eval("contains?(\"hello\", \"xyz\")"),
862 Value::Bool(false)
863 ));
864 assert!(matches!(
865 eval("starts_with?(\"hello\", \"he\")"),
866 Value::Bool(true)
867 ));
868 assert!(matches!(
869 eval("ends_with?(\"hello\", \"lo\")"),
870 Value::Bool(true)
871 ));
872 }
873
874 #[test]
875 fn text_index_answers_character_positions() {
876 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 0)"), 2);
877 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 3)"), 8);
878 assert_eq!(i("text_index(\"héllo\", \"x\", 0)"), -1);
879 assert_eq!(i("text_index(\"abc\", \"\", 3)"), 3);
880 assert_eq!(i("text_index(\"abc\", \"\", 4)"), -1);
881 assert_eq!(i("text_index(\"abc\", \"a\", -5)"), 0);
882 assert_eq!(i("text_index(\"\", \"a\", 0)"), -1);
883 }
884
885 #[test]
886 fn text_last_index_finds_the_last_start_at_or_before_from() {
887 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 99)"), 8);
888 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 7)"), 2);
889 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 1)"), 1);
890 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 0)"), 0);
891 assert_eq!(i("text_last_index(\"abc\", \"x\", 2)"), -1);
892 assert_eq!(i("text_last_index(\"abc\", \"a\", -1)"), -1);
893 assert_eq!(i("text_last_index(\"ab\", \"abc\", 0)"), -1);
894 assert_eq!(i("text_last_index(\"abc\", \"\", 9)"), 3);
895 }
896
897 #[test]
898 fn replace_and_chars() {
899 assert_eq!(s("replace(\"a-b-c\", \"-\", \"+\")"), "a+b+c");
900 assert_eq!(i("length(chars(\"abc\"))"), 3);
901 }
902
903 #[test]
906 fn reverse_is_character_wise() {
907 assert_eq!(s("reverse(\"abc\")"), "cba");
908 assert_eq!(s("reverse(\"héllo\")"), "olléh", "must not corrupt the é");
909 }
910
911 #[test]
912 fn reverse_also_works_on_a_list() {
913 assert_eq!(s("join(reverse([1, 2, 3]), \",\")"), "3,2,1");
914 }
915
916 #[test]
921 fn to_int_is_nil_on_garbage_rather_than_zero() {
922 assert_eq!(i("to_int(\"42\")"), 42);
923 assert!(
924 matches!(eval("to_int(\"banana\")"), Value::Nil),
925 "Ruby would say 0 here; a falsy nil cannot be mistaken for a result"
926 );
927 assert!(
928 matches!(eval("to_int(\"0\")"), Value::Int(0)),
929 "and a real 0 is still a real 0 — the two must stay distinguishable"
930 );
931 }
932
933 #[test]
934 fn to_int_bang_raises_on_garbage() {
935 assert_eq!(i("to_int!(\"42\")"), 42);
936 let err = crate::run("to_int!(\"banana\")").expect_err("must raise");
937 assert!(err.to_string().contains("banana"), "must name it: {err}");
938 }
939
940 #[test]
941 fn numeric_conversions_and_abs() {
942 assert_eq!(i("to_int(3.9)"), 3);
943 assert_eq!(i("abs(0 - 5)"), 5);
944 assert!(matches!(eval("to_float(\"1.5\")"), Value::Float(_)));
945 assert!(matches!(eval("to_float(\"nope\")"), Value::Nil));
946 }
947
948 #[test]
950 fn a_non_string_argument_is_a_type_error() {
951 assert!(crate::run("upcase([1, 2])").is_err());
952 assert!(crate::run("join(\"not a list\", \",\")").is_err());
953 }
954}