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 interp.register_fn(
220 "length",
221 Arity::Exact(1),
222 |a: &[Value], _h: &mut H, span| match &a[0] {
223 Value::List(items) => Ok(Value::Int(items.len() as i64)),
224 other => Ok(Value::Int(as_str(other, span)?.chars().count() as i64)),
225 },
226 );
227
228 interp.register_fn(
231 "builtin_names",
232 Arity::Exact(0),
233 |_a: &[Value], _h: &mut H, _s| {
234 Ok(Value::list(
235 crate::docs::NAMES
236 .iter()
237 .map(|n| Value::Str(std::sync::Arc::from(n.name))),
238 ))
239 },
240 );
241 interp.register_fn(
242 "chain_rules",
243 Arity::Exact(0),
244 |_a: &[Value], _h: &mut H, _s| Ok(chain_rules_value()),
245 );
246 interp.register_fn("to_s", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
247 Ok(Value::Str(render(&a[0]).into()))
248 });
249
250 interp.register_fn("upcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
251 Ok(Value::Str(as_str(&a[0], s)?.to_uppercase().into()))
252 });
253
254 interp.register_fn("downcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
255 Ok(Value::Str(as_str(&a[0], s)?.to_lowercase().into()))
256 });
257
258 interp.register_fn("trim", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
259 Ok(Value::Str(as_str(&a[0], s)?.trim().into()))
260 });
261
262 interp.register_fn(
269 "concat",
270 Arity::AtLeast(1),
271 |a: &[Value], _h: &mut H, _s| {
272 Ok(Value::Str(a.iter().map(render).collect::<String>().into()))
273 },
274 );
275
276 interp.register_fn("equal?", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
281 Ok(Value::Bool(blue_equal(&a[0], &a[1])))
282 });
283 interp.register_fn("not=", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
284 Ok(Value::Bool(!blue_equal(&a[0], &a[1])))
285 });
286
287 interp.register_fn("nil?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
293 Ok(Value::Bool(matches!(a[0], Value::Nil)))
294 });
295 interp.register_fn("list?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
296 Ok(Value::Bool(matches!(a[0], Value::List(_))))
297 });
298 interp.register_fn("map?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
299 Ok(Value::Bool(matches!(a[0], Value::Map(_))))
300 });
301 interp.register_fn("float?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
302 Ok(Value::Bool(matches!(a[0], Value::Float(_))))
303 });
304 interp.register_fn("bool?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
305 Ok(Value::Bool(matches!(a[0], Value::Bool(_))))
306 });
307
308 interp.register_fn("cdr", Arity::Exact(1), |a: &[Value], _h: &mut H, span| {
313 match &a[0] {
314 Value::List(xs) if !xs.is_empty() => Ok(Value::List(std::sync::Arc::new(xs.skip(1)))),
316 Value::Nil | Value::List(_) => Err(EvalError::native_fn(
317 std::sync::Arc::<str>::from("cdr"),
318 "cdr of empty list",
319 span,
320 )),
321 other => Err(EvalError::type_mismatch("pair", other.type_name(), span)),
322 }
323 });
324 interp.register_owned_fn("append", Arity::Any, |a: Vec<Value>, _h: &mut H, span| {
325 let mut lists = Vec::with_capacity(a.len());
332 for v in a {
333 match v {
334 Value::Nil => {}
335 Value::List(xs) => lists.push(xs),
336 other => return Err(EvalError::type_mismatch("list", other.type_name(), span)),
337 }
338 }
339 let mut lists = lists.into_iter();
340 let Some(mut out) = lists.next() else {
341 return Ok(Value::List(std::sync::Arc::new(
342 tatara_lisp_eval::List::new(),
343 )));
344 };
345 let acc = std::sync::Arc::make_mut(&mut out);
346 for xs in lists {
347 acc.append(
348 std::sync::Arc::try_unwrap(xs)
349 .unwrap_or_else(|shared| tatara_lisp_eval::List::clone(&shared)),
350 );
351 }
352 Ok(Value::List(out))
353 });
354
355 interp.register_fn("split", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
356 let text = as_str(&a[0], s)?;
357 let sep = as_str(&a[1], s)?;
358 let parts: Vec<Value> = if sep.is_empty() {
367 text.chars()
368 .map(|c| Value::Str(c.to_string().into()))
369 .collect()
370 } else {
371 text.split(sep.as_str())
372 .map(|p| Value::Str(p.into()))
373 .collect()
374 };
375 Ok(list(parts))
376 });
377
378 interp.register_fn("join", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
379 let sep = as_str(&a[1], s)?;
380 match &a[0] {
381 Value::List(items) => Ok(Value::Str(
382 items
383 .iter()
384 .map(render)
385 .collect::<Vec<_>>()
386 .join(&sep)
387 .into(),
388 )),
389 other => Err(EvalError::type_mismatch("a list", other.type_name(), s).into()),
390 }
391 });
392
393 interp.register_fn(
394 "contains?",
395 Arity::Exact(2),
396 |a: &[Value], _h: &mut H, s| {
397 Ok(Value::Bool(as_str(&a[0], s)?.contains(&as_str(&a[1], s)?)))
398 },
399 );
400
401 interp.register_fn(
402 "starts_with?",
403 Arity::Exact(2),
404 |a: &[Value], _h: &mut H, s| {
405 Ok(Value::Bool(
406 as_str(&a[0], s)?.starts_with(&as_str(&a[1], s)?),
407 ))
408 },
409 );
410
411 interp.register_fn(
412 "ends_with?",
413 Arity::Exact(2),
414 |a: &[Value], _h: &mut H, s| {
415 Ok(Value::Bool(as_str(&a[0], s)?.ends_with(&as_str(&a[1], s)?)))
416 },
417 );
418
419 interp.register_fn(
420 "text_index",
421 Arity::Exact(3),
422 |a: &[Value], _h: &mut H, s| {
423 Ok(Value::Int(text_index(
424 &as_str(&a[0], s)?,
425 &as_str(&a[1], s)?,
426 as_index(&a[2], s)?,
427 )))
428 },
429 );
430
431 interp.register_fn(
432 "text_last_index",
433 Arity::Exact(3),
434 |a: &[Value], _h: &mut H, s| {
435 Ok(Value::Int(text_last_index(
436 &as_str(&a[0], s)?,
437 &as_str(&a[1], s)?,
438 as_index(&a[2], s)?,
439 )))
440 },
441 );
442
443 interp.register_fn("replace", Arity::Exact(3), |a: &[Value], _h: &mut H, s| {
444 Ok(Value::Str(
445 as_str(&a[0], s)?
446 .replace(&as_str(&a[1], s)?, &as_str(&a[2], s)?)
447 .into(),
448 ))
449 });
450
451 interp.register_fn("reverse", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
452 match &a[0] {
453 Value::List(items) => Ok(Value::list(items.iter().rev().cloned())),
454 other => Ok(Value::Str(
457 as_str(other, s)?.chars().rev().collect::<String>().into(),
458 )),
459 }
460 });
461
462 interp.register_fn("chars", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
463 Ok(list(
464 as_str(&a[0], s)?
465 .chars()
466 .map(|c| Value::Str(c.to_string().into()))
467 .collect(),
468 ))
469 });
470
471 interp.register_fn(
478 "to_int",
479 Arity::Exact(1),
480 |a: &[Value], _h: &mut H, s| match &a[0] {
481 Value::Int(n) => Ok(Value::Int(*n)),
482 Value::Float(x) => Ok(float_to_int(*x).map_or(Value::Nil, Value::Int)),
486 other => Ok(as_str(other, s)?
487 .trim()
488 .parse::<i64>()
489 .map_or(Value::Nil, Value::Int)),
490 },
491 );
492
493 interp.register_fn(
494 "to_int!",
495 Arity::Exact(1),
496 |a: &[Value], _h: &mut H, s| match &a[0] {
497 Value::Int(n) => Ok(Value::Int(*n)),
498 Value::Float(x) => float_to_int(*x)
499 .map(Value::Int)
500 .ok_or(EvalError::IntegerOverflow {
501 op: "to_int!",
502 at: s,
503 }),
504 other => {
505 let text = as_str(other, s)?;
506 text.trim().parse::<i64>().map(Value::Int).map_err(|_| {
507 EvalError::native_fn(
508 "to_int!",
509 "`".to_string() + &text + "` is not an integer",
510 s,
511 )
512 .into()
513 })
514 }
515 },
516 );
517
518 interp.register_fn(
519 "to_float",
520 Arity::Exact(1),
521 |a: &[Value], _h: &mut H, s| match &a[0] {
522 Value::Float(x) => Ok(Value::Float(*x)),
523 Value::Int(n) => Ok(Value::Float(*n as f64)),
524 other => Ok(as_str(other, s)?
525 .trim()
526 .parse::<f64>()
527 .map_or(Value::Nil, Value::Float)),
528 },
529 );
530
531 interp.register_fn(
532 "abs",
533 Arity::Exact(1),
534 |a: &[Value], _h: &mut H, s| match &a[0] {
535 Value::Int(n) => n
538 .checked_abs()
539 .map(Value::Int)
540 .ok_or(EvalError::IntegerOverflow { op: "abs", at: s }),
541 Value::Float(x) => Ok(Value::Float(x.abs())),
542 other => Err(EvalError::type_mismatch("a number", other.type_name(), s).into()),
543 },
544 );
545
546 interp.register_fn("range", Arity::Range(1, 3), |a: &[Value], _h: &mut H, s| {
567 let (start, end, step) = match a.len() {
568 1 => (Value::Int(0), a[0].clone(), Value::Int(1)),
569 2 => (a[0].clone(), a[1].clone(), Value::Int(1)),
570 _ => (a[0].clone(), a[1].clone(), a[2].clone()),
571 };
572 range_list(start, &end, &step, s)
573 });
574
575 interp.register_fn(
582 "write_stdout",
583 Arity::Exact(1),
584 |a: &[Value], _h: &mut H, s| {
585 use std::io::Write;
586 let text = as_str(&a[0], s)?;
587 let mut out = std::io::stdout().lock();
588 out.write_all(text.as_bytes())
589 .and_then(|()| out.flush())
590 .map_err(|e| EvalError::native_fn("write_stdout", e.to_string(), s))?;
591 Ok(Value::Nil)
592 },
593 );
594 interp.register_fn(
595 "write_stderr",
596 Arity::Exact(1),
597 |a: &[Value], _h: &mut H, s| {
598 use std::io::Write;
599 let text = as_str(&a[0], s)?;
600 let mut err = std::io::stderr().lock();
601 err.write_all(text.as_bytes())
602 .and_then(|()| err.flush())
603 .map_err(|e| EvalError::native_fn("write_stderr", e.to_string(), s))?;
604 Ok(Value::Nil)
605 },
606 );
607
608 interp.register_higher_order_fn(
615 "sort_keyed",
616 Arity::Exact(2),
617 |a: &[Value], host: &mut H, caller: &tatara_lisp_eval::ffi::Caller<H>, s| {
618 let xs = match &a[1] {
619 Value::List(xs) => xs.to_vec(),
620 Value::Nil => Vec::new(),
621 other => {
622 return Err(EvalError::type_mismatch("a list", other.type_name(), s));
623 }
624 };
625 let mut keyed = Vec::with_capacity(xs.len());
626 for x in xs {
627 keyed.push((caller.apply_value(&a[0], vec![x.clone()], host, s)?, x));
628 }
629 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
630 },
631 );
632
633 interp.register_fn(
634 "error_message",
635 Arity::Exact(1),
636 |a: &[Value], _h: &mut H, s| match &a[0] {
637 Value::Error(e) => Ok(Value::Str(e.message.clone())),
638 other => Err(EvalError::type_mismatch("an error", other.type_name(), s)),
639 },
640 );
641
642 interp.register_fn(
643 "error_kind",
644 Arity::Exact(1),
645 |a: &[Value], _h: &mut H, s| match &a[0] {
646 Value::Error(e) => Ok(Value::Keyword(e.tag.clone())),
647 other => Err(EvalError::type_mismatch("an error", other.type_name(), s)),
648 },
649 );
650
651 interp.register_fn(
652 "map_entries",
653 Arity::Exact(1),
654 |a: &[Value], _h: &mut H, s| match &a[0] {
655 Value::Map(m) => {
656 let keyed = m
657 .iter()
658 .map(|(k, v)| (k.to_value(), list(vec![k.to_value(), v.clone()])))
659 .collect();
660 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
661 }
662 Value::Nil => Ok(list(Vec::new())),
663 other => Err(EvalError::type_mismatch("a map", other.type_name(), s)),
664 },
665 );
666}
667
668fn as_index(v: &Value, span: tatara_lisp::Span) -> Result<i64, EvalError> {
669 match v {
670 Value::Int(n) => Ok(*n),
671 other => Err(EvalError::type_mismatch(
672 "an integer",
673 other.type_name(),
674 span,
675 )),
676 }
677}
678
679fn text_index(text: &str, sub: &str, from: i64) -> i64 {
680 let from = usize::try_from(from.max(0)).unwrap_or(usize::MAX);
681 let start = match text.char_indices().nth(from) {
682 Some((b, _)) => b,
683 None if text.chars().count() == from => text.len(),
684 None => return -1,
685 };
686 text[start..].find(sub).map_or(-1, |b| {
687 i64::try_from(from + text[start..start + b].chars().count()).unwrap_or(i64::MAX)
688 })
689}
690
691fn text_last_index(text: &str, sub: &str, from: i64) -> i64 {
692 let Ok(from) = usize::try_from(from) else {
693 return -1;
694 };
695 let total = text.chars().count();
696 let width = sub.chars().count();
697 if width > total {
698 return -1;
699 }
700 let limit = from.min(total - width);
701 let end = text
702 .char_indices()
703 .nth(limit + width)
704 .map_or(text.len(), |(b, _)| b);
705 text[..end].rfind(sub).map_or(-1, |b| {
706 i64::try_from(text[..b].chars().count()).unwrap_or(i64::MAX)
707 })
708}
709
710fn range_num(v: &Value, s: tatara_lisp::Span) -> Result<f64, EvalError> {
712 match v {
713 Value::Int(n) => Ok(*n as f64),
714 Value::Float(x) => Ok(*x),
715 other => Err(EvalError::native_fn(
716 "range",
717 "expected a number, got ".to_string() + other.type_name(),
718 s,
719 )),
720 }
721}
722
723fn float_to_int(x: f64) -> Option<i64> {
726 const LIMIT: f64 = 9_223_372_036_854_775_808.0;
727 let t = x.trunc();
728 #[allow(clippy::cast_possible_truncation)]
729 (t.is_finite() && (-LIMIT..LIMIT).contains(&t)).then(|| t as i64)
730}
731
732fn range_add(a: &Value, b: &Value, s: tatara_lisp::Span) -> Result<Value, EvalError> {
735 match (a, b) {
736 (Value::Int(x), Value::Int(y)) => x.checked_add(*y).map(Value::Int).ok_or_else(|| {
737 EvalError::native_fn("range", "an element overflowed a 64-bit integer", s)
738 }),
739 _ => Ok(Value::Float(range_num(a, s)? + range_num(b, s)?)),
740 }
741}
742
743fn range_list(
744 start: Value,
745 end: &Value,
746 step: &Value,
747 s: tatara_lisp::Span,
748) -> Result<Value, EvalError> {
749 let end_f = range_num(end, s)?;
750 let step_f = range_num(step, s)?;
751 range_num(&start, s)?;
752 if step_f == 0.0 {
753 return Err(EvalError::native_fn(
754 "range",
755 "a step of zero never reaches the end",
756 s,
757 ));
758 }
759 let mut out = Vec::new();
760 let mut cur = start;
761 loop {
762 let c = range_num(&cur, s)?;
763 if (step_f > 0.0 && c >= end_f) || (step_f < 0.0 && c <= end_f) {
764 break;
765 }
766 let next = range_add(&cur, step, s)?;
767 out.push(cur);
768 cur = next;
769 }
770 Ok(list(out))
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
778 fn sort_keyed_is_stable_keyed_and_deep() {
779 assert_eq!(
781 ints("map(fn(p) last(p) end, sort_keyed(fn(p) first(p) end, [[2, 1], [1, 2], [2, 3], [1, 4]]))"),
782 vec![2, 4, 1, 3]
783 );
784 assert_eq!(
786 i("first(sort_keyed(fn(x) x end, reverse(range(0, 20000))))"),
787 0
788 );
789 assert_eq!(i("last(sort_keyed(fn(x) 0 - x end, range(0, 20000)))"), 0);
790 assert_eq!(ints("sort_keyed(fn(x) x end, [])"), Vec::<i64>::new());
792 assert!(crate::run("sort_keyed(fn(x) x end, [1, \"a\"])").is_err());
793 }
794
795 fn eval(src: &str) -> Value {
796 crate::run(src)
797 .unwrap_or_else(|e| panic!("{src:?}: {e}"))
798 .value
799 }
800
801 fn s(src: &str) -> String {
802 match eval(src) {
803 Value::Str(v) => v.to_string(),
804 other => panic!("{src:?} produced {other:?}"),
805 }
806 }
807
808 fn i(src: &str) -> i64 {
809 match eval(src) {
810 Value::Int(v) => v,
811 other => panic!("{src:?} produced {other:?}"),
812 }
813 }
814
815 fn ints(src: &str) -> Vec<i64> {
816 match eval(src) {
817 Value::List(xs) => xs
818 .iter()
819 .map(|v| match v {
820 Value::Int(n) => *n,
821 other => panic!("{src:?} produced a non-Int element {other:?}"),
822 })
823 .collect(),
824 other => panic!("{src:?} produced {other:?}"),
825 }
826 }
827
828 #[test]
831 fn range_answers_as_the_lisp_definition_did() {
832 assert_eq!(ints("range(5)"), vec![0, 1, 2, 3, 4]);
833 assert_eq!(ints("range(2, 6)"), vec![2, 3, 4, 5]);
834 assert_eq!(ints("range(0, 10, 2)"), vec![0, 2, 4, 6, 8]);
835 assert_eq!(ints("range(10, 0, 0 - 2)"), vec![10, 8, 6, 4, 2]);
836 assert!(ints("range(5, 5)").is_empty());
837 assert!(ints("range(5, 2)").is_empty());
838 assert!(ints("range(0)").is_empty());
839 match eval("range(0, 1, 0.5)") {
841 Value::List(xs) => {
842 assert!(matches!(xs[0], Value::Int(0)), "{xs:?}");
843 assert!(matches!(xs[1], Value::Float(x) if x == 0.5), "{xs:?}");
844 assert_eq!(xs.len(), 2);
845 }
846 other => panic!("{other:?}"),
847 }
848 }
849
850 #[test]
856 fn range_builds_long_lists_without_recursing() {
857 assert_eq!(i("length(range(0, 200000))"), 200_000);
858 assert_eq!(i("nth(199999, range(0, 200000))"), 199_999);
859 }
860
861 #[test]
864 fn range_refuses_a_zero_step_and_a_non_number() {
865 assert!(crate::run("range(0, 5, 0)").is_err());
866 assert!(crate::run("range(0, \"x\")").is_err());
867 }
868
869 #[test]
873 fn length_counts_characters_not_bytes() {
874 assert_eq!(i("length(\"hello\")"), 5);
875 assert_eq!(i("length(\"héllo\")"), 5, "must not be 6");
877 assert_eq!(i("length(\"😀\")"), 1, "must not be 4");
879 }
880
881 #[test]
884 fn a_combining_sequence_counts_scalars_not_graphemes() {
885 assert_eq!(
887 i("length(\"e\\u{301}\")"),
888 2,
889 "blue counts scalar values; Elixir's String.length would say 1"
890 );
891 }
892
893 #[test]
894 fn length_also_works_on_a_list() {
895 assert_eq!(i("length([1, 2, 3])"), 3);
896 }
897
898 #[test]
899 fn case_and_trim() {
900 assert_eq!(s("upcase(\"abc\")"), "ABC");
901 assert_eq!(s("downcase(\"ABC\")"), "abc");
902 assert_eq!(s("trim(\" hi \")"), "hi");
903 assert_eq!(s("upcase(\"é\")"), "É");
905 }
906
907 #[test]
908 fn concat_and_to_s() {
909 assert_eq!(s("concat(\"a\", \"b\")"), "ab");
910 assert_eq!(s("concat(\"n=\", 42)"), "n=42");
911 assert_eq!(s("to_s(42)"), "42");
912 assert_eq!(s("to_s(true)"), "true");
913 }
914
915 #[test]
919 fn plus_is_not_string_concatenation() {
920 assert!(
921 crate::run("\"a\" + \"b\"").is_err(),
922 "`+` must not silently concatenate — use concat"
923 );
924 }
925
926 #[test]
927 fn split_and_join() {
928 assert_eq!(i("length(split(\"a,b,c\", \",\"))"), 3);
929 assert_eq!(s("join(split(\"a,b,c\", \",\"), \"-\")"), "a-b-c");
930 assert_eq!(i("length(split(\"abc\", \"\"))"), 3);
932 }
933
934 #[test]
935 fn predicates() {
936 assert!(matches!(
937 eval("contains?(\"hello\", \"ell\")"),
938 Value::Bool(true)
939 ));
940 assert!(matches!(
941 eval("contains?(\"hello\", \"xyz\")"),
942 Value::Bool(false)
943 ));
944 assert!(matches!(
945 eval("starts_with?(\"hello\", \"he\")"),
946 Value::Bool(true)
947 ));
948 assert!(matches!(
949 eval("ends_with?(\"hello\", \"lo\")"),
950 Value::Bool(true)
951 ));
952 }
953
954 #[test]
955 fn text_index_answers_character_positions() {
956 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 0)"), 2);
957 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 3)"), 8);
958 assert_eq!(i("text_index(\"héllo\", \"x\", 0)"), -1);
959 assert_eq!(i("text_index(\"abc\", \"\", 3)"), 3);
960 assert_eq!(i("text_index(\"abc\", \"\", 4)"), -1);
961 assert_eq!(i("text_index(\"abc\", \"a\", -5)"), 0);
962 assert_eq!(i("text_index(\"\", \"a\", 0)"), -1);
963 }
964
965 #[test]
966 fn text_last_index_finds_the_last_start_at_or_before_from() {
967 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 99)"), 8);
968 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 7)"), 2);
969 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 1)"), 1);
970 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 0)"), 0);
971 assert_eq!(i("text_last_index(\"abc\", \"x\", 2)"), -1);
972 assert_eq!(i("text_last_index(\"abc\", \"a\", -1)"), -1);
973 assert_eq!(i("text_last_index(\"ab\", \"abc\", 0)"), -1);
974 assert_eq!(i("text_last_index(\"abc\", \"\", 9)"), 3);
975 }
976
977 #[test]
978 fn replace_and_chars() {
979 assert_eq!(s("replace(\"a-b-c\", \"-\", \"+\")"), "a+b+c");
980 assert_eq!(i("length(chars(\"abc\"))"), 3);
981 }
982
983 #[test]
986 fn reverse_is_character_wise() {
987 assert_eq!(s("reverse(\"abc\")"), "cba");
988 assert_eq!(s("reverse(\"héllo\")"), "olléh", "must not corrupt the é");
989 }
990
991 #[test]
992 fn reverse_also_works_on_a_list() {
993 assert_eq!(s("join(reverse([1, 2, 3]), \",\")"), "3,2,1");
994 }
995
996 #[test]
1001 fn to_int_is_nil_on_garbage_rather_than_zero() {
1002 assert_eq!(i("to_int(\"42\")"), 42);
1003 assert!(
1004 matches!(eval("to_int(\"banana\")"), Value::Nil),
1005 "Ruby would say 0 here; a falsy nil cannot be mistaken for a result"
1006 );
1007 assert!(
1008 matches!(eval("to_int(\"0\")"), Value::Int(0)),
1009 "and a real 0 is still a real 0 — the two must stay distinguishable"
1010 );
1011 }
1012
1013 #[test]
1014 fn to_int_bang_raises_on_garbage() {
1015 assert_eq!(i("to_int!(\"42\")"), 42);
1016 let err = crate::run("to_int!(\"banana\")").expect_err("must raise");
1017 assert!(err.to_string().contains("banana"), "must name it: {err}");
1018 }
1019
1020 #[test]
1021 fn numeric_conversions_and_abs() {
1022 assert_eq!(i("to_int(3.9)"), 3);
1023 assert_eq!(i("abs(0 - 5)"), 5);
1024 assert!(matches!(eval("to_float(\"1.5\")"), Value::Float(_)));
1025 assert!(matches!(eval("to_float(\"nope\")"), Value::Nil));
1026 }
1027
1028 #[test]
1030 fn a_non_string_argument_is_a_type_error() {
1031 assert!(crate::run("upcase([1, 2])").is_err());
1032 assert!(crate::run("join(\"not a list\", \",\")").is_err());
1033 }
1034}