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 Value::Error(e) => crate::errors::describe(e),
98 other => literal(other),
99 }
100}
101
102fn literal(v: &Value) -> String {
106 match v {
107 Value::Nil => "nil".into(),
108 Value::Bool(b) => b.to_string(),
109 Value::Int(n) => n.to_string(),
110 Value::Float(x) => float_text(*x),
111 Value::Str(s) => quoted(s),
112 Value::Symbol(s) => s.to_string(),
113 Value::Keyword(s) => format!(":{s}"),
114 Value::List(items) => {
115 let parts: Vec<String> = items.iter().map(literal).collect();
116 format!("[{}]", parts.join(", "))
117 }
118 Value::Map(m) => {
119 let mut entries: Vec<String> = m
120 .iter()
121 .map(|(k, v)| format!("{} {}", key_text(k), literal(v)))
122 .collect();
123 entries.sort();
124 format!("{{{}}}", entries.join(", "))
125 }
126 other => other.type_name().to_string(),
127 }
128}
129
130fn key_text(k: &tatara_lisp_eval::value::MapKey) -> String {
133 use tatara_lisp_eval::value::MapKey;
134 match k {
135 MapKey::Keyword(s) | MapKey::Symbol(s) => format!("{s}:"),
136 MapKey::Str(s) => format!("{} =>", quoted(s)),
137 MapKey::Int(n) => format!("{n} =>"),
138 MapKey::Float(bits) => format!("{} =>", float_text(f64::from_bits(*bits))),
139 MapKey::Bool(b) => format!("{b} =>"),
140 MapKey::Nil => "nil =>".into(),
141 }
142}
143
144fn float_text(x: f64) -> String {
146 if x.is_finite() && x.fract() == 0.0 && x.abs() < 1e16 {
147 format!("{x:.1}")
148 } else {
149 format!("{x}")
150 }
151}
152
153fn quoted(s: &str) -> String {
154 let mut out = String::with_capacity(s.len() + 2);
155 out.push('"');
156 for c in s.chars() {
157 match c {
158 '"' => out.push_str("\\\""),
159 '\\' => out.push_str("\\\\"),
160 '\n' => out.push_str("\\n"),
161 '\t' => out.push_str("\\t"),
162 c => out.push(c),
163 }
164 }
165 out.push('"');
166 out
167}
168
169fn list(items: Vec<Value>) -> Value {
170 Value::list(items)
171}
172
173pub fn install_blue_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
175 crate::errors::install(interp);
176 interp.register_fn(
181 "length",
182 Arity::Exact(1),
183 |a: &[Value], _h: &mut H, span| match &a[0] {
184 Value::List(items) => Ok(Value::Int(items.len() as i64)),
185 other => Ok(Value::Int(as_str(other, span)?.chars().count() as i64)),
186 },
187 );
188
189 interp.register_fn(
192 "builtin_names",
193 Arity::Exact(0),
194 |_a: &[Value], _h: &mut H, _s| {
195 Ok(Value::list(
196 crate::docs::NAMES
197 .iter()
198 .map(|n| Value::Str(std::sync::Arc::from(n.name))),
199 ))
200 },
201 );
202 interp.register_fn("to_s", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
203 Ok(Value::Str(render(&a[0]).into()))
204 });
205
206 interp.register_fn("upcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
207 Ok(Value::Str(as_str(&a[0], s)?.to_uppercase().into()))
208 });
209
210 interp.register_fn("downcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
211 Ok(Value::Str(as_str(&a[0], s)?.to_lowercase().into()))
212 });
213
214 interp.register_fn("trim", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
215 Ok(Value::Str(as_str(&a[0], s)?.trim().into()))
216 });
217
218 interp.register_fn(
225 "concat",
226 Arity::AtLeast(1),
227 |a: &[Value], _h: &mut H, _s| {
228 Ok(Value::Str(a.iter().map(render).collect::<String>().into()))
229 },
230 );
231
232 interp.register_fn("equal?", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
237 Ok(Value::Bool(blue_equal(&a[0], &a[1])))
238 });
239 interp.register_fn("not=", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
240 Ok(Value::Bool(!blue_equal(&a[0], &a[1])))
241 });
242
243 interp.register_fn("nil?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
249 Ok(Value::Bool(matches!(a[0], Value::Nil)))
250 });
251 interp.register_fn("list?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
252 Ok(Value::Bool(matches!(a[0], Value::List(_))))
253 });
254 interp.register_fn("map?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
255 Ok(Value::Bool(matches!(a[0], Value::Map(_))))
256 });
257 interp.register_fn("float?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
258 Ok(Value::Bool(matches!(a[0], Value::Float(_))))
259 });
260 interp.register_fn("bool?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
261 Ok(Value::Bool(matches!(a[0], Value::Bool(_))))
262 });
263
264 interp.register_fn("cdr", Arity::Exact(1), |a: &[Value], _h: &mut H, span| {
269 match &a[0] {
270 Value::List(xs) if !xs.is_empty() => Ok(Value::List(std::sync::Arc::new(xs.skip(1)))),
272 Value::Nil | Value::List(_) => Err(EvalError::native_fn(
273 std::sync::Arc::<str>::from("cdr"),
274 "cdr of empty list",
275 span,
276 )),
277 other => Err(EvalError::type_mismatch("pair", other.type_name(), span)),
278 }
279 });
280 interp.register_owned_fn("append", Arity::Any, |a: Vec<Value>, _h: &mut H, span| {
281 let mut lists = Vec::with_capacity(a.len());
288 for v in a {
289 match v {
290 Value::Nil => {}
291 Value::List(xs) => lists.push(xs),
292 other => return Err(EvalError::type_mismatch("list", other.type_name(), span)),
293 }
294 }
295 let mut lists = lists.into_iter();
296 let Some(mut out) = lists.next() else {
297 return Ok(Value::List(std::sync::Arc::new(
298 tatara_lisp_eval::List::new(),
299 )));
300 };
301 let acc = std::sync::Arc::make_mut(&mut out);
302 for xs in lists {
303 acc.append(
304 std::sync::Arc::try_unwrap(xs)
305 .unwrap_or_else(|shared| tatara_lisp_eval::List::clone(&shared)),
306 );
307 }
308 Ok(Value::List(out))
309 });
310
311 interp.register_fn("split", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
312 let text = as_str(&a[0], s)?;
313 let sep = as_str(&a[1], s)?;
314 let parts: Vec<Value> = if sep.is_empty() {
323 text.chars()
324 .map(|c| Value::Str(c.to_string().into()))
325 .collect()
326 } else {
327 text.split(sep.as_str())
328 .map(|p| Value::Str(p.into()))
329 .collect()
330 };
331 Ok(list(parts))
332 });
333
334 interp.register_fn("join", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
335 let sep = as_str(&a[1], s)?;
336 match &a[0] {
337 Value::List(items) => Ok(Value::Str(
338 items
339 .iter()
340 .map(render)
341 .collect::<Vec<_>>()
342 .join(&sep)
343 .into(),
344 )),
345 other => Err(EvalError::type_mismatch("a list", other.type_name(), s).into()),
346 }
347 });
348
349 interp.register_fn(
350 "contains?",
351 Arity::Exact(2),
352 |a: &[Value], _h: &mut H, s| {
353 Ok(Value::Bool(as_str(&a[0], s)?.contains(&as_str(&a[1], s)?)))
354 },
355 );
356
357 interp.register_fn(
358 "starts_with?",
359 Arity::Exact(2),
360 |a: &[Value], _h: &mut H, s| {
361 Ok(Value::Bool(
362 as_str(&a[0], s)?.starts_with(&as_str(&a[1], s)?),
363 ))
364 },
365 );
366
367 interp.register_fn(
368 "ends_with?",
369 Arity::Exact(2),
370 |a: &[Value], _h: &mut H, s| {
371 Ok(Value::Bool(as_str(&a[0], s)?.ends_with(&as_str(&a[1], s)?)))
372 },
373 );
374
375 interp.register_fn(
376 "text_index",
377 Arity::Exact(3),
378 |a: &[Value], _h: &mut H, s| {
379 Ok(Value::Int(text_index(
380 &as_str(&a[0], s)?,
381 &as_str(&a[1], s)?,
382 as_index(&a[2], s)?,
383 )))
384 },
385 );
386
387 interp.register_fn(
388 "text_last_index",
389 Arity::Exact(3),
390 |a: &[Value], _h: &mut H, s| {
391 Ok(Value::Int(text_last_index(
392 &as_str(&a[0], s)?,
393 &as_str(&a[1], s)?,
394 as_index(&a[2], s)?,
395 )))
396 },
397 );
398
399 interp.register_fn("replace", Arity::Exact(3), |a: &[Value], _h: &mut H, s| {
400 Ok(Value::Str(
401 as_str(&a[0], s)?
402 .replace(&as_str(&a[1], s)?, &as_str(&a[2], s)?)
403 .into(),
404 ))
405 });
406
407 interp.register_fn("reverse", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
408 match &a[0] {
409 Value::List(items) => Ok(Value::list(items.iter().rev().cloned())),
410 other => Ok(Value::Str(
413 as_str(other, s)?.chars().rev().collect::<String>().into(),
414 )),
415 }
416 });
417
418 interp.register_fn("chars", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
419 Ok(list(
420 as_str(&a[0], s)?
421 .chars()
422 .map(|c| Value::Str(c.to_string().into()))
423 .collect(),
424 ))
425 });
426
427 interp.register_fn(
434 "to_int",
435 Arity::Exact(1),
436 |a: &[Value], _h: &mut H, s| match &a[0] {
437 Value::Int(n) => Ok(Value::Int(*n)),
438 Value::Float(x) => Ok(float_to_int(*x).map_or(Value::Nil, Value::Int)),
442 other => Ok(as_str(other, s)?
443 .trim()
444 .parse::<i64>()
445 .map_or(Value::Nil, Value::Int)),
446 },
447 );
448
449 interp.register_fn(
450 "to_int!",
451 Arity::Exact(1),
452 |a: &[Value], _h: &mut H, s| match &a[0] {
453 Value::Int(n) => Ok(Value::Int(*n)),
454 Value::Float(x) => float_to_int(*x)
455 .map(Value::Int)
456 .ok_or(EvalError::IntegerOverflow {
457 op: "to_int!",
458 at: s,
459 }),
460 other => {
461 let text = as_str(other, s)?;
462 text.trim().parse::<i64>().map(Value::Int).map_err(|_| {
463 EvalError::native_fn(
464 "to_int!",
465 "`".to_string() + &text + "` is not an integer",
466 s,
467 )
468 .into()
469 })
470 }
471 },
472 );
473
474 interp.register_fn(
475 "to_float",
476 Arity::Exact(1),
477 |a: &[Value], _h: &mut H, s| match &a[0] {
478 Value::Float(x) => Ok(Value::Float(*x)),
479 Value::Int(n) => Ok(Value::Float(*n as f64)),
480 other => Ok(as_str(other, s)?
481 .trim()
482 .parse::<f64>()
483 .map_or(Value::Nil, Value::Float)),
484 },
485 );
486
487 interp.register_fn(
488 "abs",
489 Arity::Exact(1),
490 |a: &[Value], _h: &mut H, s| match &a[0] {
491 Value::Int(n) => n
494 .checked_abs()
495 .map(Value::Int)
496 .ok_or(EvalError::IntegerOverflow { op: "abs", at: s }),
497 Value::Float(x) => Ok(Value::Float(x.abs())),
498 other => Err(EvalError::type_mismatch("a number", other.type_name(), s).into()),
499 },
500 );
501
502 interp.register_fn("range", Arity::Range(1, 3), |a: &[Value], _h: &mut H, s| {
523 let (start, end, step) = match a.len() {
524 1 => (Value::Int(0), a[0].clone(), Value::Int(1)),
525 2 => (a[0].clone(), a[1].clone(), Value::Int(1)),
526 _ => (a[0].clone(), a[1].clone(), a[2].clone()),
527 };
528 range_list(start, &end, &step, s)
529 });
530
531 interp.register_fn(
538 "write_stdout",
539 Arity::Exact(1),
540 |a: &[Value], _h: &mut H, s| {
541 use std::io::Write;
542 let text = as_str(&a[0], s)?;
543 let mut out = std::io::stdout().lock();
544 out.write_all(text.as_bytes())
545 .and_then(|()| out.flush())
546 .map_err(|e| EvalError::native_fn("write_stdout", e.to_string(), s))?;
547 Ok(Value::Nil)
548 },
549 );
550 interp.register_fn(
551 "write_stderr",
552 Arity::Exact(1),
553 |a: &[Value], _h: &mut H, s| {
554 use std::io::Write;
555 let text = as_str(&a[0], s)?;
556 let mut err = std::io::stderr().lock();
557 err.write_all(text.as_bytes())
558 .and_then(|()| err.flush())
559 .map_err(|e| EvalError::native_fn("write_stderr", e.to_string(), s))?;
560 Ok(Value::Nil)
561 },
562 );
563
564 interp.register_higher_order_fn(
571 "sort_keyed",
572 Arity::Exact(2),
573 |a: &[Value], host: &mut H, caller: &tatara_lisp_eval::ffi::Caller<H>, s| {
574 let xs = match &a[1] {
575 Value::List(xs) => xs.to_vec(),
576 Value::Nil => Vec::new(),
577 other => {
578 return Err(EvalError::type_mismatch("a list", other.type_name(), s));
579 }
580 };
581 let mut keyed = Vec::with_capacity(xs.len());
582 for x in xs {
583 keyed.push((caller.apply_value(&a[0], vec![x.clone()], host, s)?, x));
584 }
585 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
586 },
587 );
588
589 interp.register_fn(
590 "error_message",
591 Arity::Exact(1),
592 |a: &[Value], _h: &mut H, s| match &a[0] {
593 Value::Error(e) => Ok(Value::Str(e.message.clone())),
594 other => Err(EvalError::type_mismatch("an error", other.type_name(), s)),
595 },
596 );
597
598 interp.register_fn(
599 "error_kind",
600 Arity::Exact(1),
601 |a: &[Value], _h: &mut H, s| match &a[0] {
602 Value::Error(e) => Ok(Value::Keyword(e.tag.clone())),
603 other => Err(EvalError::type_mismatch("an error", other.type_name(), s)),
604 },
605 );
606
607 interp.register_fn(
608 "map_entries",
609 Arity::Exact(1),
610 |a: &[Value], _h: &mut H, s| match &a[0] {
611 Value::Map(m) => {
612 let keyed = m
613 .iter()
614 .map(|(k, v)| (k.to_value(), list(vec![k.to_value(), v.clone()])))
615 .collect();
616 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
617 }
618 Value::Nil => Ok(list(Vec::new())),
619 other => Err(EvalError::type_mismatch("a map", other.type_name(), s)),
620 },
621 );
622}
623
624fn as_index(v: &Value, span: tatara_lisp::Span) -> Result<i64, EvalError> {
625 match v {
626 Value::Int(n) => Ok(*n),
627 other => Err(EvalError::type_mismatch(
628 "an integer",
629 other.type_name(),
630 span,
631 )),
632 }
633}
634
635fn text_index(text: &str, sub: &str, from: i64) -> i64 {
636 let from = usize::try_from(from.max(0)).unwrap_or(usize::MAX);
637 let start = match text.char_indices().nth(from) {
638 Some((b, _)) => b,
639 None if text.chars().count() == from => text.len(),
640 None => return -1,
641 };
642 text[start..].find(sub).map_or(-1, |b| {
643 i64::try_from(from + text[start..start + b].chars().count()).unwrap_or(i64::MAX)
644 })
645}
646
647fn text_last_index(text: &str, sub: &str, from: i64) -> i64 {
648 let Ok(from) = usize::try_from(from) else {
649 return -1;
650 };
651 let total = text.chars().count();
652 let width = sub.chars().count();
653 if width > total {
654 return -1;
655 }
656 let limit = from.min(total - width);
657 let end = text
658 .char_indices()
659 .nth(limit + width)
660 .map_or(text.len(), |(b, _)| b);
661 text[..end].rfind(sub).map_or(-1, |b| {
662 i64::try_from(text[..b].chars().count()).unwrap_or(i64::MAX)
663 })
664}
665
666fn range_num(v: &Value, s: tatara_lisp::Span) -> Result<f64, EvalError> {
668 match v {
669 Value::Int(n) => Ok(*n as f64),
670 Value::Float(x) => Ok(*x),
671 other => Err(EvalError::native_fn(
672 "range",
673 "expected a number, got ".to_string() + other.type_name(),
674 s,
675 )),
676 }
677}
678
679fn float_to_int(x: f64) -> Option<i64> {
682 const LIMIT: f64 = 9_223_372_036_854_775_808.0;
683 let t = x.trunc();
684 #[allow(clippy::cast_possible_truncation)]
685 (t.is_finite() && (-LIMIT..LIMIT).contains(&t)).then(|| t as i64)
686}
687
688fn range_add(a: &Value, b: &Value, s: tatara_lisp::Span) -> Result<Value, EvalError> {
691 match (a, b) {
692 (Value::Int(x), Value::Int(y)) => x.checked_add(*y).map(Value::Int).ok_or_else(|| {
693 EvalError::native_fn("range", "an element overflowed a 64-bit integer", s)
694 }),
695 _ => Ok(Value::Float(range_num(a, s)? + range_num(b, s)?)),
696 }
697}
698
699fn range_list(
700 start: Value,
701 end: &Value,
702 step: &Value,
703 s: tatara_lisp::Span,
704) -> Result<Value, EvalError> {
705 let end_f = range_num(end, s)?;
706 let step_f = range_num(step, s)?;
707 range_num(&start, s)?;
708 if step_f == 0.0 {
709 return Err(EvalError::native_fn(
710 "range",
711 "a step of zero never reaches the end",
712 s,
713 ));
714 }
715 let mut out = Vec::new();
716 let mut cur = start;
717 loop {
718 let c = range_num(&cur, s)?;
719 if (step_f > 0.0 && c >= end_f) || (step_f < 0.0 && c <= end_f) {
720 break;
721 }
722 let next = range_add(&cur, step, s)?;
723 out.push(cur);
724 cur = next;
725 }
726 Ok(list(out))
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732
733 #[test]
734 fn sort_keyed_is_stable_keyed_and_deep() {
735 assert_eq!(
737 ints("map(fn(p) last(p) end, sort_keyed(fn(p) first(p) end, [[2, 1], [1, 2], [2, 3], [1, 4]]))"),
738 vec![2, 4, 1, 3]
739 );
740 assert_eq!(
742 i("first(sort_keyed(fn(x) x end, reverse(range(0, 20000))))"),
743 0
744 );
745 assert_eq!(i("last(sort_keyed(fn(x) 0 - x end, range(0, 20000)))"), 0);
746 assert_eq!(ints("sort_keyed(fn(x) x end, [])"), Vec::<i64>::new());
748 assert!(crate::run("sort_keyed(fn(x) x end, [1, \"a\"])").is_err());
749 }
750
751 fn eval(src: &str) -> Value {
752 crate::run(src)
753 .unwrap_or_else(|e| panic!("{src:?}: {e}"))
754 .value
755 }
756
757 fn s(src: &str) -> String {
758 match eval(src) {
759 Value::Str(v) => v.to_string(),
760 other => panic!("{src:?} produced {other:?}"),
761 }
762 }
763
764 fn i(src: &str) -> i64 {
765 match eval(src) {
766 Value::Int(v) => v,
767 other => panic!("{src:?} produced {other:?}"),
768 }
769 }
770
771 fn ints(src: &str) -> Vec<i64> {
772 match eval(src) {
773 Value::List(xs) => xs
774 .iter()
775 .map(|v| match v {
776 Value::Int(n) => *n,
777 other => panic!("{src:?} produced a non-Int element {other:?}"),
778 })
779 .collect(),
780 other => panic!("{src:?} produced {other:?}"),
781 }
782 }
783
784 #[test]
787 fn range_answers_as_the_lisp_definition_did() {
788 assert_eq!(ints("range(5)"), vec![0, 1, 2, 3, 4]);
789 assert_eq!(ints("range(2, 6)"), vec![2, 3, 4, 5]);
790 assert_eq!(ints("range(0, 10, 2)"), vec![0, 2, 4, 6, 8]);
791 assert_eq!(ints("range(10, 0, 0 - 2)"), vec![10, 8, 6, 4, 2]);
792 assert!(ints("range(5, 5)").is_empty());
793 assert!(ints("range(5, 2)").is_empty());
794 assert!(ints("range(0)").is_empty());
795 match eval("range(0, 1, 0.5)") {
797 Value::List(xs) => {
798 assert!(matches!(xs[0], Value::Int(0)), "{xs:?}");
799 assert!(matches!(xs[1], Value::Float(x) if x == 0.5), "{xs:?}");
800 assert_eq!(xs.len(), 2);
801 }
802 other => panic!("{other:?}"),
803 }
804 }
805
806 #[test]
812 fn range_builds_long_lists_without_recursing() {
813 assert_eq!(i("length(range(0, 200000))"), 200_000);
814 assert_eq!(i("nth(199999, range(0, 200000))"), 199_999);
815 }
816
817 #[test]
820 fn range_refuses_a_zero_step_and_a_non_number() {
821 assert!(crate::run("range(0, 5, 0)").is_err());
822 assert!(crate::run("range(0, \"x\")").is_err());
823 }
824
825 #[test]
829 fn length_counts_characters_not_bytes() {
830 assert_eq!(i("length(\"hello\")"), 5);
831 assert_eq!(i("length(\"héllo\")"), 5, "must not be 6");
833 assert_eq!(i("length(\"😀\")"), 1, "must not be 4");
835 }
836
837 #[test]
840 fn a_combining_sequence_counts_scalars_not_graphemes() {
841 assert_eq!(
843 i("length(\"e\\u{301}\")"),
844 2,
845 "blue counts scalar values; Elixir's String.length would say 1"
846 );
847 }
848
849 #[test]
850 fn length_also_works_on_a_list() {
851 assert_eq!(i("length([1, 2, 3])"), 3);
852 }
853
854 #[test]
855 fn case_and_trim() {
856 assert_eq!(s("upcase(\"abc\")"), "ABC");
857 assert_eq!(s("downcase(\"ABC\")"), "abc");
858 assert_eq!(s("trim(\" hi \")"), "hi");
859 assert_eq!(s("upcase(\"é\")"), "É");
861 }
862
863 #[test]
864 fn concat_and_to_s() {
865 assert_eq!(s("concat(\"a\", \"b\")"), "ab");
866 assert_eq!(s("concat(\"n=\", 42)"), "n=42");
867 assert_eq!(s("to_s(42)"), "42");
868 assert_eq!(s("to_s(true)"), "true");
869 }
870
871 #[test]
875 fn plus_is_not_string_concatenation() {
876 assert!(
877 crate::run("\"a\" + \"b\"").is_err(),
878 "`+` must not silently concatenate — use concat"
879 );
880 }
881
882 #[test]
883 fn split_and_join() {
884 assert_eq!(i("length(split(\"a,b,c\", \",\"))"), 3);
885 assert_eq!(s("join(split(\"a,b,c\", \",\"), \"-\")"), "a-b-c");
886 assert_eq!(i("length(split(\"abc\", \"\"))"), 3);
888 }
889
890 #[test]
891 fn predicates() {
892 assert!(matches!(
893 eval("contains?(\"hello\", \"ell\")"),
894 Value::Bool(true)
895 ));
896 assert!(matches!(
897 eval("contains?(\"hello\", \"xyz\")"),
898 Value::Bool(false)
899 ));
900 assert!(matches!(
901 eval("starts_with?(\"hello\", \"he\")"),
902 Value::Bool(true)
903 ));
904 assert!(matches!(
905 eval("ends_with?(\"hello\", \"lo\")"),
906 Value::Bool(true)
907 ));
908 }
909
910 #[test]
911 fn text_index_answers_character_positions() {
912 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 0)"), 2);
913 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 3)"), 8);
914 assert_eq!(i("text_index(\"héllo\", \"x\", 0)"), -1);
915 assert_eq!(i("text_index(\"abc\", \"\", 3)"), 3);
916 assert_eq!(i("text_index(\"abc\", \"\", 4)"), -1);
917 assert_eq!(i("text_index(\"abc\", \"a\", -5)"), 0);
918 assert_eq!(i("text_index(\"\", \"a\", 0)"), -1);
919 }
920
921 #[test]
922 fn text_last_index_finds_the_last_start_at_or_before_from() {
923 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 99)"), 8);
924 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 7)"), 2);
925 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 1)"), 1);
926 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 0)"), 0);
927 assert_eq!(i("text_last_index(\"abc\", \"x\", 2)"), -1);
928 assert_eq!(i("text_last_index(\"abc\", \"a\", -1)"), -1);
929 assert_eq!(i("text_last_index(\"ab\", \"abc\", 0)"), -1);
930 assert_eq!(i("text_last_index(\"abc\", \"\", 9)"), 3);
931 }
932
933 #[test]
934 fn replace_and_chars() {
935 assert_eq!(s("replace(\"a-b-c\", \"-\", \"+\")"), "a+b+c");
936 assert_eq!(i("length(chars(\"abc\"))"), 3);
937 }
938
939 #[test]
942 fn reverse_is_character_wise() {
943 assert_eq!(s("reverse(\"abc\")"), "cba");
944 assert_eq!(s("reverse(\"héllo\")"), "olléh", "must not corrupt the é");
945 }
946
947 #[test]
948 fn reverse_also_works_on_a_list() {
949 assert_eq!(s("join(reverse([1, 2, 3]), \",\")"), "3,2,1");
950 }
951
952 #[test]
957 fn to_int_is_nil_on_garbage_rather_than_zero() {
958 assert_eq!(i("to_int(\"42\")"), 42);
959 assert!(
960 matches!(eval("to_int(\"banana\")"), Value::Nil),
961 "Ruby would say 0 here; a falsy nil cannot be mistaken for a result"
962 );
963 assert!(
964 matches!(eval("to_int(\"0\")"), Value::Int(0)),
965 "and a real 0 is still a real 0 — the two must stay distinguishable"
966 );
967 }
968
969 #[test]
970 fn to_int_bang_raises_on_garbage() {
971 assert_eq!(i("to_int!(\"42\")"), 42);
972 let err = crate::run("to_int!(\"banana\")").expect_err("must raise");
973 assert!(err.to_string().contains("banana"), "must name it: {err}");
974 }
975
976 #[test]
977 fn numeric_conversions_and_abs() {
978 assert_eq!(i("to_int(3.9)"), 3);
979 assert_eq!(i("abs(0 - 5)"), 5);
980 assert!(matches!(eval("to_float(\"1.5\")"), Value::Float(_)));
981 assert!(matches!(eval("to_float(\"nope\")"), Value::Nil));
982 }
983
984 #[test]
986 fn a_non_string_argument_is_a_type_error() {
987 assert!(crate::run("upcase([1, 2])").is_err());
988 assert!(crate::run("join(\"not a list\", \",\")").is_err());
989 }
990}