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 chain_rules_value() -> Value {
94 use blue_lang_check::chains::{cases, Case, Kind, RULES};
95 let sym = |s: &str| Value::Symbol(std::sync::Arc::from(s));
96 let text = |s: &str| Value::Str(std::sync::Arc::from(s));
97 let hole = |n: usize| (0..n).map(|_| sym("x")).collect::<Vec<_>>();
98 let lambda = |body: Value| Value::list([sym("lambda"), Value::list([sym("x")]), body]);
99 let call = |name: &str, args: Vec<Value>| Value::list(std::iter::once(sym(name)).chain(args));
100 Value::list(RULES.iter().map(|rule| {
101 let rows = cases(rule).into_iter().map(|case| match case {
102 Case::Call { inner, argc } => {
103 let inner_call = call(inner, hole(argc));
104 Value::list([
105 text(&format!("{}({}(x))", rule.outer, inner)),
106 Value::Nil,
107 lambda(call(rule.outer, vec![inner_call.clone()])),
108 lambda(inner_call),
109 ])
110 }
111 Case::Literal(kind) => Value::list([
112 text(&format!("{}(<literal>)", rule.outer)),
113 Value::Keyword(std::sync::Arc::from(match kind {
114 Kind::Text => "text",
115 Kind::Int => "int",
116 Kind::Float => "float",
117 })),
118 lambda(call(rule.outer, vec![sym("x")])),
119 lambda(sym("x")),
120 ]),
121 });
122 Value::list([
123 text(rule.id),
124 text(rule.slug),
125 text(rule.outer),
126 text(rule.soundness),
127 Value::list(rows),
128 ])
129 }))
130}
131
132fn render(v: &Value) -> String {
133 match v {
134 Value::Nil => String::new(),
135 Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => s.to_string(),
136 Value::Error(e) => crate::errors::describe(e),
137 other => literal(other),
138 }
139}
140
141fn literal(v: &Value) -> String {
145 match v {
146 Value::Nil => "nil".into(),
147 Value::Bool(b) => b.to_string(),
148 Value::Int(n) => n.to_string(),
149 Value::Float(x) => float_text(*x),
150 Value::Str(s) => quoted(s),
151 Value::Symbol(s) => s.to_string(),
152 Value::Keyword(s) => format!(":{s}"),
153 Value::List(items) => {
154 let parts: Vec<String> = items.iter().map(literal).collect();
155 format!("[{}]", parts.join(", "))
156 }
157 Value::Map(m) => {
158 let mut entries: Vec<String> = m
159 .iter()
160 .map(|(k, v)| format!("{} {}", key_text(k), literal(v)))
161 .collect();
162 entries.sort();
163 format!("{{{}}}", entries.join(", "))
164 }
165 other => other.type_name().to_string(),
166 }
167}
168
169fn key_text(k: &tatara_lisp_eval::value::MapKey) -> String {
172 use tatara_lisp_eval::value::MapKey;
173 match k {
174 MapKey::Keyword(s) | MapKey::Symbol(s) => format!("{s}:"),
175 MapKey::Str(s) => format!("{} =>", quoted(s)),
176 MapKey::Int(n) => format!("{n} =>"),
177 MapKey::Float(bits) => format!("{} =>", float_text(f64::from_bits(*bits))),
178 MapKey::Bool(b) => format!("{b} =>"),
179 MapKey::Nil => "nil =>".into(),
180 }
181}
182
183fn float_text(x: f64) -> String {
185 if x.is_finite() && x.fract() == 0.0 && x.abs() < 1e16 {
186 format!("{x:.1}")
187 } else {
188 format!("{x}")
189 }
190}
191
192fn quoted(s: &str) -> String {
193 let mut out = String::with_capacity(s.len() + 2);
194 out.push('"');
195 for c in s.chars() {
196 match c {
197 '"' => out.push_str("\\\""),
198 '\\' => out.push_str("\\\\"),
199 '\n' => out.push_str("\\n"),
200 '\t' => out.push_str("\\t"),
201 c => out.push(c),
202 }
203 }
204 out.push('"');
205 out
206}
207
208fn list(items: Vec<Value>) -> Value {
209 Value::list(items)
210}
211
212pub fn install_blue_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
214 crate::errors::install(interp);
215 crate::paths::install(interp);
216 crate::spread::install(interp);
217 interp.register_fn(
222 "length",
223 Arity::Exact(1),
224 |a: &[Value], _h: &mut H, span| match &a[0] {
225 Value::List(items) => Ok(Value::Int(items.len() as i64)),
226 other => Ok(Value::Int(as_str(other, span)?.chars().count() as i64)),
227 },
228 );
229
230 interp.register_fn(
233 "builtin_names",
234 Arity::Exact(0),
235 |_a: &[Value], _h: &mut H, _s| {
236 Ok(Value::list(
237 crate::docs::NAMES
238 .iter()
239 .map(|n| Value::Str(std::sync::Arc::from(n.name))),
240 ))
241 },
242 );
243 interp.register_fn(
244 "chain_rules",
245 Arity::Exact(0),
246 |_a: &[Value], _h: &mut H, _s| Ok(chain_rules_value()),
247 );
248 interp.register_fn("to_s", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
249 Ok(Value::Str(render(&a[0]).into()))
250 });
251
252 interp.register_fn("upcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
253 Ok(Value::Str(as_str(&a[0], s)?.to_uppercase().into()))
254 });
255
256 interp.register_fn("downcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
257 Ok(Value::Str(as_str(&a[0], s)?.to_lowercase().into()))
258 });
259
260 interp.register_fn("trim", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
261 Ok(Value::Str(as_str(&a[0], s)?.trim().into()))
262 });
263
264 interp.register_fn(
271 "concat",
272 Arity::AtLeast(1),
273 |a: &[Value], _h: &mut H, _s| {
274 Ok(Value::Str(a.iter().map(render).collect::<String>().into()))
275 },
276 );
277
278 interp.register_fn("equal?", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
283 Ok(Value::Bool(blue_equal(&a[0], &a[1])))
284 });
285 interp.register_fn("not=", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
286 Ok(Value::Bool(!blue_equal(&a[0], &a[1])))
287 });
288
289 interp.register_fn("nil?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
295 Ok(Value::Bool(matches!(a[0], Value::Nil)))
296 });
297 interp.register_fn("list?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
298 Ok(Value::Bool(matches!(a[0], Value::List(_))))
299 });
300 interp.register_fn("map?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
301 Ok(Value::Bool(matches!(a[0], Value::Map(_))))
302 });
303 interp.register_fn("float?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
304 Ok(Value::Bool(matches!(a[0], Value::Float(_))))
305 });
306 interp.register_fn("bool?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
307 Ok(Value::Bool(matches!(a[0], Value::Bool(_))))
308 });
309
310 interp.register_fn("cdr", Arity::Exact(1), |a: &[Value], _h: &mut H, span| {
315 match &a[0] {
316 Value::List(xs) if !xs.is_empty() => Ok(Value::List(std::sync::Arc::new(xs.skip(1)))),
318 Value::Nil | Value::List(_) => Err(EvalError::native_fn(
319 std::sync::Arc::<str>::from("cdr"),
320 "cdr of empty list",
321 span,
322 )),
323 other => Err(EvalError::type_mismatch("pair", other.type_name(), span)),
324 }
325 });
326 interp.register_owned_fn("append", Arity::Any, |a: Vec<Value>, _h: &mut H, span| {
327 let mut lists = Vec::with_capacity(a.len());
334 for v in a {
335 match v {
336 Value::Nil => {}
337 Value::List(xs) => lists.push(xs),
338 other => return Err(EvalError::type_mismatch("list", other.type_name(), span)),
339 }
340 }
341 let mut lists = lists.into_iter();
342 let Some(mut out) = lists.next() else {
343 return Ok(Value::List(std::sync::Arc::new(
344 tatara_lisp_eval::List::new(),
345 )));
346 };
347 let acc = std::sync::Arc::make_mut(&mut out);
348 for xs in lists {
349 acc.append(
350 std::sync::Arc::try_unwrap(xs)
351 .unwrap_or_else(|shared| tatara_lisp_eval::List::clone(&shared)),
352 );
353 }
354 Ok(Value::List(out))
355 });
356
357 interp.register_fn("split", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
358 let text = as_str(&a[0], s)?;
359 let sep = as_str(&a[1], s)?;
360 let parts: Vec<Value> = if sep.is_empty() {
369 text.chars()
370 .map(|c| Value::Str(c.to_string().into()))
371 .collect()
372 } else {
373 text.split(sep.as_str())
374 .map(|p| Value::Str(p.into()))
375 .collect()
376 };
377 Ok(list(parts))
378 });
379
380 interp.register_fn("join", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
381 let sep = as_str(&a[1], s)?;
382 match &a[0] {
383 Value::List(items) => Ok(Value::Str(
384 items
385 .iter()
386 .map(render)
387 .collect::<Vec<_>>()
388 .join(&sep)
389 .into(),
390 )),
391 other => Err(EvalError::type_mismatch("a list", other.type_name(), s).into()),
392 }
393 });
394
395 interp.register_fn(
396 "contains?",
397 Arity::Exact(2),
398 |a: &[Value], _h: &mut H, s| {
399 Ok(Value::Bool(as_str(&a[0], s)?.contains(&as_str(&a[1], s)?)))
400 },
401 );
402
403 interp.register_fn(
404 "starts_with?",
405 Arity::Exact(2),
406 |a: &[Value], _h: &mut H, s| {
407 Ok(Value::Bool(
408 as_str(&a[0], s)?.starts_with(&as_str(&a[1], s)?),
409 ))
410 },
411 );
412
413 interp.register_fn(
414 "ends_with?",
415 Arity::Exact(2),
416 |a: &[Value], _h: &mut H, s| {
417 Ok(Value::Bool(as_str(&a[0], s)?.ends_with(&as_str(&a[1], s)?)))
418 },
419 );
420
421 interp.register_fn(
422 "text_index",
423 Arity::Exact(3),
424 |a: &[Value], _h: &mut H, s| {
425 Ok(Value::Int(text_index(
426 &as_str(&a[0], s)?,
427 &as_str(&a[1], s)?,
428 as_index(&a[2], s)?,
429 )))
430 },
431 );
432
433 interp.register_fn(
434 "text_last_index",
435 Arity::Exact(3),
436 |a: &[Value], _h: &mut H, s| {
437 Ok(Value::Int(text_last_index(
438 &as_str(&a[0], s)?,
439 &as_str(&a[1], s)?,
440 as_index(&a[2], s)?,
441 )))
442 },
443 );
444
445 interp.register_fn("replace", Arity::Exact(3), |a: &[Value], _h: &mut H, s| {
446 Ok(Value::Str(
447 as_str(&a[0], s)?
448 .replace(&as_str(&a[1], s)?, &as_str(&a[2], s)?)
449 .into(),
450 ))
451 });
452
453 interp.register_fn("reverse", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
454 match &a[0] {
455 Value::List(items) => Ok(Value::list(items.iter().rev().cloned())),
456 other => Ok(Value::Str(
459 as_str(other, s)?.chars().rev().collect::<String>().into(),
460 )),
461 }
462 });
463
464 interp.register_fn("chars", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
465 Ok(list(
466 as_str(&a[0], s)?
467 .chars()
468 .map(|c| Value::Str(c.to_string().into()))
469 .collect(),
470 ))
471 });
472
473 interp.register_fn(
480 "to_int",
481 Arity::Exact(1),
482 |a: &[Value], _h: &mut H, s| match &a[0] {
483 Value::Int(n) => Ok(Value::Int(*n)),
484 Value::Float(x) => Ok(float_to_int(*x).map_or(Value::Nil, Value::Int)),
488 other => Ok(as_str(other, s)?
489 .trim()
490 .parse::<i64>()
491 .map_or(Value::Nil, Value::Int)),
492 },
493 );
494
495 interp.register_fn(
496 "to_int!",
497 Arity::Exact(1),
498 |a: &[Value], _h: &mut H, s| match &a[0] {
499 Value::Int(n) => Ok(Value::Int(*n)),
500 Value::Float(x) => float_to_int(*x)
501 .map(Value::Int)
502 .ok_or(EvalError::IntegerOverflow {
503 op: "to_int!",
504 at: s,
505 }),
506 other => {
507 let text = as_str(other, s)?;
508 text.trim().parse::<i64>().map(Value::Int).map_err(|_| {
509 EvalError::native_fn(
510 "to_int!",
511 "`".to_string() + &text + "` is not an integer",
512 s,
513 )
514 .into()
515 })
516 }
517 },
518 );
519
520 interp.register_fn(
521 "to_float",
522 Arity::Exact(1),
523 |a: &[Value], _h: &mut H, s| match &a[0] {
524 Value::Float(x) => Ok(Value::Float(*x)),
525 Value::Int(n) => Ok(Value::Float(*n as f64)),
526 other => Ok(as_str(other, s)?
527 .trim()
528 .parse::<f64>()
529 .map_or(Value::Nil, Value::Float)),
530 },
531 );
532
533 interp.register_fn(
534 "abs",
535 Arity::Exact(1),
536 |a: &[Value], _h: &mut H, s| match &a[0] {
537 Value::Int(n) => n
540 .checked_abs()
541 .map(Value::Int)
542 .ok_or(EvalError::IntegerOverflow { op: "abs", at: s }),
543 Value::Float(x) => Ok(Value::Float(x.abs())),
544 other => Err(EvalError::type_mismatch("a number", other.type_name(), s).into()),
545 },
546 );
547
548 interp.register_fn("range", Arity::Range(1, 3), |a: &[Value], _h: &mut H, s| {
569 let (start, end, step) = match a.len() {
570 1 => (Value::Int(0), a[0].clone(), Value::Int(1)),
571 2 => (a[0].clone(), a[1].clone(), Value::Int(1)),
572 _ => (a[0].clone(), a[1].clone(), a[2].clone()),
573 };
574 range_list(start, &end, &step, s)
575 });
576
577 interp.register_fn(
584 "write_stdout",
585 Arity::Exact(1),
586 |a: &[Value], _h: &mut H, s| {
587 use std::io::Write;
588 let text = as_str(&a[0], s)?;
589 let mut out = std::io::stdout().lock();
590 out.write_all(text.as_bytes())
591 .and_then(|()| out.flush())
592 .map_err(|e| EvalError::native_fn("write_stdout", e.to_string(), s))?;
593 Ok(Value::Nil)
594 },
595 );
596 interp.register_fn(
597 "write_stderr",
598 Arity::Exact(1),
599 |a: &[Value], _h: &mut H, s| {
600 use std::io::Write;
601 let text = as_str(&a[0], s)?;
602 let mut err = std::io::stderr().lock();
603 err.write_all(text.as_bytes())
604 .and_then(|()| err.flush())
605 .map_err(|e| EvalError::native_fn("write_stderr", e.to_string(), s))?;
606 Ok(Value::Nil)
607 },
608 );
609
610 interp.register_higher_order_fn(
617 "sort_keyed",
618 Arity::Exact(2),
619 |a: &[Value], host: &mut H, caller: &tatara_lisp_eval::ffi::Caller<H>, s| {
620 let xs = match &a[1] {
621 Value::List(xs) => xs.to_vec(),
622 Value::Nil => Vec::new(),
623 other => {
624 return Err(EvalError::type_mismatch("a list", other.type_name(), s));
625 }
626 };
627 let mut keyed = Vec::with_capacity(xs.len());
628 for x in xs {
629 keyed.push((caller.apply_value(&a[0], vec![x.clone()], host, s)?, x));
630 }
631 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
632 },
633 );
634
635 interp.register_fn(
636 "error_message",
637 Arity::Exact(1),
638 |a: &[Value], _h: &mut H, s| match &a[0] {
639 Value::Error(e) => Ok(Value::Str(e.message.clone())),
640 other => Err(EvalError::type_mismatch("an error", other.type_name(), s)),
641 },
642 );
643
644 interp.register_fn(
645 "error_kind",
646 Arity::Exact(1),
647 |a: &[Value], _h: &mut H, s| match &a[0] {
648 Value::Error(e) => Ok(Value::Keyword(e.tag.clone())),
649 other => Err(EvalError::type_mismatch("an error", other.type_name(), s)),
650 },
651 );
652
653 interp.register_fn(
654 "map_entries",
655 Arity::Exact(1),
656 |a: &[Value], _h: &mut H, s| match &a[0] {
657 Value::Map(m) => {
658 let keyed = m
659 .iter()
660 .map(|(k, v)| (k.to_value(), list(vec![k.to_value(), v.clone()])))
661 .collect();
662 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
663 }
664 Value::Nil => Ok(list(Vec::new())),
665 other => Err(EvalError::type_mismatch("a map", other.type_name(), s)),
666 },
667 );
668}
669
670fn as_index(v: &Value, span: tatara_lisp::Span) -> Result<i64, EvalError> {
671 match v {
672 Value::Int(n) => Ok(*n),
673 other => Err(EvalError::type_mismatch(
674 "an integer",
675 other.type_name(),
676 span,
677 )),
678 }
679}
680
681fn text_index(text: &str, sub: &str, from: i64) -> i64 {
682 let from = usize::try_from(from.max(0)).unwrap_or(usize::MAX);
683 let start = match text.char_indices().nth(from) {
684 Some((b, _)) => b,
685 None if text.chars().count() == from => text.len(),
686 None => return -1,
687 };
688 text[start..].find(sub).map_or(-1, |b| {
689 i64::try_from(from + text[start..start + b].chars().count()).unwrap_or(i64::MAX)
690 })
691}
692
693fn text_last_index(text: &str, sub: &str, from: i64) -> i64 {
694 let Ok(from) = usize::try_from(from) else {
695 return -1;
696 };
697 let total = text.chars().count();
698 let width = sub.chars().count();
699 if width > total {
700 return -1;
701 }
702 let limit = from.min(total - width);
703 let end = text
704 .char_indices()
705 .nth(limit + width)
706 .map_or(text.len(), |(b, _)| b);
707 text[..end].rfind(sub).map_or(-1, |b| {
708 i64::try_from(text[..b].chars().count()).unwrap_or(i64::MAX)
709 })
710}
711
712fn range_num(v: &Value, s: tatara_lisp::Span) -> Result<f64, EvalError> {
714 match v {
715 Value::Int(n) => Ok(*n as f64),
716 Value::Float(x) => Ok(*x),
717 other => Err(EvalError::native_fn(
718 "range",
719 "expected a number, got ".to_string() + other.type_name(),
720 s,
721 )),
722 }
723}
724
725fn float_to_int(x: f64) -> Option<i64> {
728 const LIMIT: f64 = 9_223_372_036_854_775_808.0;
729 let t = x.trunc();
730 #[allow(clippy::cast_possible_truncation)]
731 (t.is_finite() && (-LIMIT..LIMIT).contains(&t)).then(|| t as i64)
732}
733
734fn range_add(a: &Value, b: &Value, s: tatara_lisp::Span) -> Result<Value, EvalError> {
737 match (a, b) {
738 (Value::Int(x), Value::Int(y)) => x.checked_add(*y).map(Value::Int).ok_or_else(|| {
739 EvalError::native_fn("range", "an element overflowed a 64-bit integer", s)
740 }),
741 _ => Ok(Value::Float(range_num(a, s)? + range_num(b, s)?)),
742 }
743}
744
745fn range_list(
746 start: Value,
747 end: &Value,
748 step: &Value,
749 s: tatara_lisp::Span,
750) -> Result<Value, EvalError> {
751 let end_f = range_num(end, s)?;
752 let step_f = range_num(step, s)?;
753 range_num(&start, s)?;
754 if step_f == 0.0 {
755 return Err(EvalError::native_fn(
756 "range",
757 "a step of zero never reaches the end",
758 s,
759 ));
760 }
761 let mut out = Vec::new();
762 let mut cur = start;
763 loop {
764 let c = range_num(&cur, s)?;
765 if (step_f > 0.0 && c >= end_f) || (step_f < 0.0 && c <= end_f) {
766 break;
767 }
768 let next = range_add(&cur, step, s)?;
769 out.push(cur);
770 cur = next;
771 }
772 Ok(list(out))
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778
779 #[test]
780 fn sort_keyed_is_stable_keyed_and_deep() {
781 assert_eq!(
783 ints("map(fn(p) last(p) end, sort_keyed(fn(p) first(p) end, [[2, 1], [1, 2], [2, 3], [1, 4]]))"),
784 vec![2, 4, 1, 3]
785 );
786 assert_eq!(
788 i("first(sort_keyed(fn(x) x end, reverse(range(0, 20000))))"),
789 0
790 );
791 assert_eq!(i("last(sort_keyed(fn(x) 0 - x end, range(0, 20000)))"), 0);
792 assert_eq!(ints("sort_keyed(fn(x) x end, [])"), Vec::<i64>::new());
794 assert!(crate::run("sort_keyed(fn(x) x end, [1, \"a\"])").is_err());
795 }
796
797 fn eval(src: &str) -> Value {
798 crate::run(src)
799 .unwrap_or_else(|e| panic!("{src:?}: {e}"))
800 .value
801 }
802
803 fn s(src: &str) -> String {
804 match eval(src) {
805 Value::Str(v) => v.to_string(),
806 other => panic!("{src:?} produced {other:?}"),
807 }
808 }
809
810 fn i(src: &str) -> i64 {
811 match eval(src) {
812 Value::Int(v) => v,
813 other => panic!("{src:?} produced {other:?}"),
814 }
815 }
816
817 fn ints(src: &str) -> Vec<i64> {
818 match eval(src) {
819 Value::List(xs) => xs
820 .iter()
821 .map(|v| match v {
822 Value::Int(n) => *n,
823 other => panic!("{src:?} produced a non-Int element {other:?}"),
824 })
825 .collect(),
826 other => panic!("{src:?} produced {other:?}"),
827 }
828 }
829
830 #[test]
833 fn range_answers_as_the_lisp_definition_did() {
834 assert_eq!(ints("range(5)"), vec![0, 1, 2, 3, 4]);
835 assert_eq!(ints("range(2, 6)"), vec![2, 3, 4, 5]);
836 assert_eq!(ints("range(0, 10, 2)"), vec![0, 2, 4, 6, 8]);
837 assert_eq!(ints("range(10, 0, 0 - 2)"), vec![10, 8, 6, 4, 2]);
838 assert!(ints("range(5, 5)").is_empty());
839 assert!(ints("range(5, 2)").is_empty());
840 assert!(ints("range(0)").is_empty());
841 match eval("range(0, 1, 0.5)") {
843 Value::List(xs) => {
844 assert!(matches!(xs[0], Value::Int(0)), "{xs:?}");
845 assert!(matches!(xs[1], Value::Float(x) if x == 0.5), "{xs:?}");
846 assert_eq!(xs.len(), 2);
847 }
848 other => panic!("{other:?}"),
849 }
850 }
851
852 #[test]
858 fn range_builds_long_lists_without_recursing() {
859 assert_eq!(i("length(range(0, 200000))"), 200_000);
860 assert_eq!(i("nth(199999, range(0, 200000))"), 199_999);
861 }
862
863 #[test]
866 fn range_refuses_a_zero_step_and_a_non_number() {
867 assert!(crate::run("range(0, 5, 0)").is_err());
868 assert!(crate::run("range(0, \"x\")").is_err());
869 }
870
871 #[test]
875 fn length_counts_characters_not_bytes() {
876 assert_eq!(i("length(\"hello\")"), 5);
877 assert_eq!(i("length(\"héllo\")"), 5, "must not be 6");
879 assert_eq!(i("length(\"😀\")"), 1, "must not be 4");
881 }
882
883 #[test]
886 fn a_combining_sequence_counts_scalars_not_graphemes() {
887 assert_eq!(
889 i("length(\"e\\u{301}\")"),
890 2,
891 "blue counts scalar values; Elixir's String.length would say 1"
892 );
893 }
894
895 #[test]
896 fn length_also_works_on_a_list() {
897 assert_eq!(i("length([1, 2, 3])"), 3);
898 }
899
900 #[test]
901 fn case_and_trim() {
902 assert_eq!(s("upcase(\"abc\")"), "ABC");
903 assert_eq!(s("downcase(\"ABC\")"), "abc");
904 assert_eq!(s("trim(\" hi \")"), "hi");
905 assert_eq!(s("upcase(\"é\")"), "É");
907 }
908
909 #[test]
910 fn concat_and_to_s() {
911 assert_eq!(s("concat(\"a\", \"b\")"), "ab");
912 assert_eq!(s("concat(\"n=\", 42)"), "n=42");
913 assert_eq!(s("to_s(42)"), "42");
914 assert_eq!(s("to_s(true)"), "true");
915 }
916
917 #[test]
921 fn plus_is_not_string_concatenation() {
922 assert!(
923 crate::run("\"a\" + \"b\"").is_err(),
924 "`+` must not silently concatenate — use concat"
925 );
926 }
927
928 #[test]
929 fn split_and_join() {
930 assert_eq!(i("length(split(\"a,b,c\", \",\"))"), 3);
931 assert_eq!(s("join(split(\"a,b,c\", \",\"), \"-\")"), "a-b-c");
932 assert_eq!(i("length(split(\"abc\", \"\"))"), 3);
934 }
935
936 #[test]
937 fn predicates() {
938 assert!(matches!(
939 eval("contains?(\"hello\", \"ell\")"),
940 Value::Bool(true)
941 ));
942 assert!(matches!(
943 eval("contains?(\"hello\", \"xyz\")"),
944 Value::Bool(false)
945 ));
946 assert!(matches!(
947 eval("starts_with?(\"hello\", \"he\")"),
948 Value::Bool(true)
949 ));
950 assert!(matches!(
951 eval("ends_with?(\"hello\", \"lo\")"),
952 Value::Bool(true)
953 ));
954 }
955
956 #[test]
957 fn text_index_answers_character_positions() {
958 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 0)"), 2);
959 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 3)"), 8);
960 assert_eq!(i("text_index(\"héllo\", \"x\", 0)"), -1);
961 assert_eq!(i("text_index(\"abc\", \"\", 3)"), 3);
962 assert_eq!(i("text_index(\"abc\", \"\", 4)"), -1);
963 assert_eq!(i("text_index(\"abc\", \"a\", -5)"), 0);
964 assert_eq!(i("text_index(\"\", \"a\", 0)"), -1);
965 }
966
967 #[test]
968 fn text_last_index_finds_the_last_start_at_or_before_from() {
969 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 99)"), 8);
970 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 7)"), 2);
971 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 1)"), 1);
972 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 0)"), 0);
973 assert_eq!(i("text_last_index(\"abc\", \"x\", 2)"), -1);
974 assert_eq!(i("text_last_index(\"abc\", \"a\", -1)"), -1);
975 assert_eq!(i("text_last_index(\"ab\", \"abc\", 0)"), -1);
976 assert_eq!(i("text_last_index(\"abc\", \"\", 9)"), 3);
977 }
978
979 #[test]
980 fn replace_and_chars() {
981 assert_eq!(s("replace(\"a-b-c\", \"-\", \"+\")"), "a+b+c");
982 assert_eq!(i("length(chars(\"abc\"))"), 3);
983 }
984
985 #[test]
988 fn reverse_is_character_wise() {
989 assert_eq!(s("reverse(\"abc\")"), "cba");
990 assert_eq!(s("reverse(\"héllo\")"), "olléh", "must not corrupt the é");
991 }
992
993 #[test]
994 fn reverse_also_works_on_a_list() {
995 assert_eq!(s("join(reverse([1, 2, 3]), \",\")"), "3,2,1");
996 }
997
998 #[test]
1003 fn to_int_is_nil_on_garbage_rather_than_zero() {
1004 assert_eq!(i("to_int(\"42\")"), 42);
1005 assert!(
1006 matches!(eval("to_int(\"banana\")"), Value::Nil),
1007 "Ruby would say 0 here; a falsy nil cannot be mistaken for a result"
1008 );
1009 assert!(
1010 matches!(eval("to_int(\"0\")"), Value::Int(0)),
1011 "and a real 0 is still a real 0 — the two must stay distinguishable"
1012 );
1013 }
1014
1015 #[test]
1016 fn to_int_bang_raises_on_garbage() {
1017 assert_eq!(i("to_int!(\"42\")"), 42);
1018 let err = crate::run("to_int!(\"banana\")").expect_err("must raise");
1019 assert!(err.to_string().contains("banana"), "must name it: {err}");
1020 }
1021
1022 #[test]
1023 fn numeric_conversions_and_abs() {
1024 assert_eq!(i("to_int(3.9)"), 3);
1025 assert_eq!(i("abs(0 - 5)"), 5);
1026 assert!(matches!(eval("to_float(\"1.5\")"), Value::Float(_)));
1027 assert!(matches!(eval("to_float(\"nope\")"), Value::Nil));
1028 }
1029
1030 #[test]
1032 fn a_non_string_argument_is_a_type_error() {
1033 assert!(crate::run("upcase([1, 2])").is_err());
1034 assert!(crate::run("join(\"not a list\", \",\")").is_err());
1035 }
1036}