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
52fn render(v: &Value) -> String {
54 match v {
55 Value::Nil => String::new(),
56 Value::Bool(b) => b.to_string(),
57 Value::Int(n) => n.to_string(),
58 Value::Float(x) => x.to_string(),
59 Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => s.to_string(),
60 Value::List(items) => items.iter().map(render).collect::<Vec<_>>().join(" "),
61 other => other.type_name().to_string(),
62 }
63}
64
65fn list(items: Vec<Value>) -> Value {
66 Value::List(std::sync::Arc::new(items))
67}
68
69pub fn install_blue_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
71 interp.register_fn(
76 "length",
77 Arity::Exact(1),
78 |a: &[Value], _h: &mut H, span| match &a[0] {
79 Value::List(items) => Ok(Value::Int(items.len() as i64)),
80 other => Ok(Value::Int(as_str(other, span)?.chars().count() as i64)),
81 },
82 );
83
84 interp.register_fn("to_s", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
85 Ok(Value::Str(render(&a[0]).into()))
86 });
87
88 interp.register_fn("upcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
89 Ok(Value::Str(as_str(&a[0], s)?.to_uppercase().into()))
90 });
91
92 interp.register_fn("downcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
93 Ok(Value::Str(as_str(&a[0], s)?.to_lowercase().into()))
94 });
95
96 interp.register_fn("trim", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
97 Ok(Value::Str(as_str(&a[0], s)?.trim().into()))
98 });
99
100 interp.register_fn("concat", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
105 let mut out = render(&a[0]);
106 out.push_str(&render(&a[1]));
107 Ok(Value::Str(out.into()))
108 });
109
110 interp.register_fn("split", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
111 let text = as_str(&a[0], s)?;
112 let sep = as_str(&a[1], s)?;
113 let parts: Vec<Value> = if sep.is_empty() {
117 text.chars()
118 .map(|c| Value::Str(c.to_string().into()))
119 .collect()
120 } else {
121 text.split(sep.as_str())
122 .map(|p| Value::Str(p.into()))
123 .collect()
124 };
125 Ok(list(parts))
126 });
127
128 interp.register_fn("join", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
129 let sep = as_str(&a[1], s)?;
130 match &a[0] {
131 Value::List(items) => Ok(Value::Str(
132 items
133 .iter()
134 .map(render)
135 .collect::<Vec<_>>()
136 .join(&sep)
137 .into(),
138 )),
139 other => Err(EvalError::type_mismatch("a list", other.type_name(), s).into()),
140 }
141 });
142
143 interp.register_fn(
144 "contains?",
145 Arity::Exact(2),
146 |a: &[Value], _h: &mut H, s| {
147 Ok(Value::Bool(as_str(&a[0], s)?.contains(&as_str(&a[1], s)?)))
148 },
149 );
150
151 interp.register_fn(
152 "starts_with?",
153 Arity::Exact(2),
154 |a: &[Value], _h: &mut H, s| {
155 Ok(Value::Bool(
156 as_str(&a[0], s)?.starts_with(&as_str(&a[1], s)?),
157 ))
158 },
159 );
160
161 interp.register_fn(
162 "ends_with?",
163 Arity::Exact(2),
164 |a: &[Value], _h: &mut H, s| {
165 Ok(Value::Bool(as_str(&a[0], s)?.ends_with(&as_str(&a[1], s)?)))
166 },
167 );
168
169 interp.register_fn("replace", Arity::Exact(3), |a: &[Value], _h: &mut H, s| {
170 Ok(Value::Str(
171 as_str(&a[0], s)?
172 .replace(&as_str(&a[1], s)?, &as_str(&a[2], s)?)
173 .into(),
174 ))
175 });
176
177 interp.register_fn("reverse", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
178 match &a[0] {
179 Value::List(items) => {
180 let mut v = items.as_ref().clone();
181 v.reverse();
182 Ok(list(v))
183 }
184 other => Ok(Value::Str(
187 as_str(other, s)?.chars().rev().collect::<String>().into(),
188 )),
189 }
190 });
191
192 interp.register_fn("chars", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
193 Ok(list(
194 as_str(&a[0], s)?
195 .chars()
196 .map(|c| Value::Str(c.to_string().into()))
197 .collect(),
198 ))
199 });
200
201 interp.register_fn(
208 "to_int",
209 Arity::Exact(1),
210 |a: &[Value], _h: &mut H, s| match &a[0] {
211 Value::Int(n) => Ok(Value::Int(*n)),
212 Value::Float(x) => Ok(Value::Int(*x as i64)),
213 other => Ok(as_str(other, s)?
214 .trim()
215 .parse::<i64>()
216 .map_or(Value::Nil, Value::Int)),
217 },
218 );
219
220 interp.register_fn(
221 "to_int!",
222 Arity::Exact(1),
223 |a: &[Value], _h: &mut H, s| match &a[0] {
224 Value::Int(n) => Ok(Value::Int(*n)),
225 Value::Float(x) => Ok(Value::Int(*x as i64)),
226 other => {
227 let text = as_str(other, s)?;
228 text.trim().parse::<i64>().map(Value::Int).map_err(|_| {
229 EvalError::native_fn(
230 "to_int!",
231 "`".to_string() + &text + "` is not an integer",
232 s,
233 )
234 .into()
235 })
236 }
237 },
238 );
239
240 interp.register_fn(
241 "to_float",
242 Arity::Exact(1),
243 |a: &[Value], _h: &mut H, s| match &a[0] {
244 Value::Float(x) => Ok(Value::Float(*x)),
245 Value::Int(n) => Ok(Value::Float(*n as f64)),
246 other => Ok(as_str(other, s)?
247 .trim()
248 .parse::<f64>()
249 .map_or(Value::Nil, Value::Float)),
250 },
251 );
252
253 interp.register_fn(
254 "abs",
255 Arity::Exact(1),
256 |a: &[Value], _h: &mut H, s| match &a[0] {
257 Value::Int(n) => Ok(Value::Int(n.abs())),
258 Value::Float(x) => Ok(Value::Float(x.abs())),
259 other => Err(EvalError::type_mismatch("a number", other.type_name(), s).into()),
260 },
261 );
262
263 interp.register_fn(
284 "range",
285 Arity::Range(1, 3),
286 |a: &[Value], _h: &mut H, s| {
287 let (start, end, step) = match a.len() {
288 1 => (Value::Int(0), a[0].clone(), Value::Int(1)),
289 2 => (a[0].clone(), a[1].clone(), Value::Int(1)),
290 _ => (a[0].clone(), a[1].clone(), a[2].clone()),
291 };
292 range_list(start, &end, &step, s)
293 },
294 );
295
296 interp.register_fn("write_stdout", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
303 use std::io::Write;
304 let text = as_str(&a[0], s)?;
305 let mut out = std::io::stdout().lock();
306 out.write_all(text.as_bytes())
307 .and_then(|()| out.flush())
308 .map_err(|e| EvalError::native_fn("write_stdout", e.to_string(), s))?;
309 Ok(Value::Nil)
310 });
311 interp.register_fn("write_stderr", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
312 use std::io::Write;
313 let text = as_str(&a[0], s)?;
314 let mut err = std::io::stderr().lock();
315 err.write_all(text.as_bytes())
316 .and_then(|()| err.flush())
317 .map_err(|e| EvalError::native_fn("write_stderr", e.to_string(), s))?;
318 Ok(Value::Nil)
319 });
320
321 interp.register_higher_order_fn(
328 "sort_keyed",
329 Arity::Exact(2),
330 |a: &[Value], host: &mut H, caller: &tatara_lisp_eval::ffi::Caller<H>, s| {
331 let xs = match &a[1] {
332 Value::List(xs) => xs.as_ref().clone(),
333 Value::Nil => Vec::new(),
334 other => {
335 return Err(EvalError::type_mismatch("a list", other.type_name(), s));
336 }
337 };
338 let mut keyed = Vec::with_capacity(xs.len());
339 for x in xs {
340 keyed.push((caller.apply_value(&a[0], vec![x.clone()], host, s)?, x));
341 }
342 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
343 },
344 );
345}
346
347fn range_num(v: &Value, s: tatara_lisp::Span) -> Result<f64, EvalError> {
349 match v {
350 Value::Int(n) => Ok(*n as f64),
351 Value::Float(x) => Ok(*x),
352 other => Err(EvalError::native_fn(
353 "range",
354 "expected a number, got ".to_string() + other.type_name(),
355 s,
356 )),
357 }
358}
359
360fn range_add(a: &Value, b: &Value, s: tatara_lisp::Span) -> Result<Value, EvalError> {
363 match (a, b) {
364 (Value::Int(x), Value::Int(y)) => x.checked_add(*y).map(Value::Int).ok_or_else(|| {
365 EvalError::native_fn("range", "an element overflowed a 64-bit integer", s)
366 }),
367 _ => Ok(Value::Float(range_num(a, s)? + range_num(b, s)?)),
368 }
369}
370
371fn range_list(
372 start: Value,
373 end: &Value,
374 step: &Value,
375 s: tatara_lisp::Span,
376) -> Result<Value, EvalError> {
377 let end_f = range_num(end, s)?;
378 let step_f = range_num(step, s)?;
379 range_num(&start, s)?;
380 if step_f == 0.0 {
381 return Err(EvalError::native_fn(
382 "range",
383 "a step of zero never reaches the end",
384 s,
385 ));
386 }
387 let mut out = Vec::new();
388 let mut cur = start;
389 loop {
390 let c = range_num(&cur, s)?;
391 if (step_f > 0.0 && c >= end_f) || (step_f < 0.0 && c <= end_f) {
392 break;
393 }
394 let next = range_add(&cur, step, s)?;
395 out.push(cur);
396 cur = next;
397 }
398 Ok(list(out))
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn sort_keyed_is_stable_keyed_and_deep() {
407 assert_eq!(
409 ints("map(fn(p) last(p) end, sort_keyed(fn(p) first(p) end, [[2, 1], [1, 2], [2, 3], [1, 4]]))"),
410 vec![2, 4, 1, 3]
411 );
412 assert_eq!(
414 i("first(sort_keyed(fn(x) x end, reverse(range(0, 20000))))"),
415 0
416 );
417 assert_eq!(i("last(sort_keyed(fn(x) 0 - x end, range(0, 20000)))"), 0);
418 assert_eq!(ints("sort_keyed(fn(x) x end, [])"), Vec::<i64>::new());
420 assert!(crate::run("sort_keyed(fn(x) x end, [1, \"a\"])").is_err());
421 }
422
423 fn eval(src: &str) -> Value {
424 crate::run(src)
425 .unwrap_or_else(|e| panic!("{src:?}: {e}"))
426 .value
427 }
428
429 fn s(src: &str) -> String {
430 match eval(src) {
431 Value::Str(v) => v.to_string(),
432 other => panic!("{src:?} produced {other:?}"),
433 }
434 }
435
436 fn i(src: &str) -> i64 {
437 match eval(src) {
438 Value::Int(v) => v,
439 other => panic!("{src:?} produced {other:?}"),
440 }
441 }
442
443 fn ints(src: &str) -> Vec<i64> {
444 match eval(src) {
445 Value::List(xs) => xs
446 .iter()
447 .map(|v| match v {
448 Value::Int(n) => *n,
449 other => panic!("{src:?} produced a non-Int element {other:?}"),
450 })
451 .collect(),
452 other => panic!("{src:?} produced {other:?}"),
453 }
454 }
455
456 #[test]
459 fn range_answers_as_the_lisp_definition_did() {
460 assert_eq!(ints("range(5)"), vec![0, 1, 2, 3, 4]);
461 assert_eq!(ints("range(2, 6)"), vec![2, 3, 4, 5]);
462 assert_eq!(ints("range(0, 10, 2)"), vec![0, 2, 4, 6, 8]);
463 assert_eq!(ints("range(10, 0, 0 - 2)"), vec![10, 8, 6, 4, 2]);
464 assert!(ints("range(5, 5)").is_empty());
465 assert!(ints("range(5, 2)").is_empty());
466 assert!(ints("range(0)").is_empty());
467 match eval("range(0, 1, 0.5)") {
469 Value::List(xs) => {
470 assert!(matches!(xs[0], Value::Int(0)), "{xs:?}");
471 assert!(matches!(xs[1], Value::Float(x) if x == 0.5), "{xs:?}");
472 assert_eq!(xs.len(), 2);
473 }
474 other => panic!("{other:?}"),
475 }
476 }
477
478 #[test]
484 fn range_builds_long_lists_without_recursing() {
485 assert_eq!(i("length(range(0, 200000))"), 200_000);
486 assert_eq!(i("nth(199999, range(0, 200000))"), 199_999);
487 }
488
489 #[test]
492 fn range_refuses_a_zero_step_and_a_non_number() {
493 assert!(crate::run("range(0, 5, 0)").is_err());
494 assert!(crate::run("range(0, \"x\")").is_err());
495 }
496
497 #[test]
501 fn length_counts_characters_not_bytes() {
502 assert_eq!(i("length(\"hello\")"), 5);
503 assert_eq!(i("length(\"héllo\")"), 5, "must not be 6");
505 assert_eq!(i("length(\"😀\")"), 1, "must not be 4");
507 }
508
509 #[test]
512 fn a_combining_sequence_counts_scalars_not_graphemes() {
513 assert_eq!(
515 i("length(\"e\\u{301}\")"),
516 2,
517 "blue counts scalar values; Elixir's String.length would say 1"
518 );
519 }
520
521 #[test]
522 fn length_also_works_on_a_list() {
523 assert_eq!(i("length([1, 2, 3])"), 3);
524 }
525
526 #[test]
527 fn case_and_trim() {
528 assert_eq!(s("upcase(\"abc\")"), "ABC");
529 assert_eq!(s("downcase(\"ABC\")"), "abc");
530 assert_eq!(s("trim(\" hi \")"), "hi");
531 assert_eq!(s("upcase(\"é\")"), "É");
533 }
534
535 #[test]
536 fn concat_and_to_s() {
537 assert_eq!(s("concat(\"a\", \"b\")"), "ab");
538 assert_eq!(s("concat(\"n=\", 42)"), "n=42");
539 assert_eq!(s("to_s(42)"), "42");
540 assert_eq!(s("to_s(true)"), "true");
541 }
542
543 #[test]
547 fn plus_is_not_string_concatenation() {
548 assert!(
549 crate::run("\"a\" + \"b\"").is_err(),
550 "`+` must not silently concatenate — use concat"
551 );
552 }
553
554 #[test]
555 fn split_and_join() {
556 assert_eq!(i("length(split(\"a,b,c\", \",\"))"), 3);
557 assert_eq!(s("join(split(\"a,b,c\", \",\"), \"-\")"), "a-b-c");
558 assert_eq!(i("length(split(\"abc\", \"\"))"), 3);
560 }
561
562 #[test]
563 fn predicates() {
564 assert!(matches!(
565 eval("contains?(\"hello\", \"ell\")"),
566 Value::Bool(true)
567 ));
568 assert!(matches!(
569 eval("contains?(\"hello\", \"xyz\")"),
570 Value::Bool(false)
571 ));
572 assert!(matches!(
573 eval("starts_with?(\"hello\", \"he\")"),
574 Value::Bool(true)
575 ));
576 assert!(matches!(
577 eval("ends_with?(\"hello\", \"lo\")"),
578 Value::Bool(true)
579 ));
580 }
581
582 #[test]
583 fn replace_and_chars() {
584 assert_eq!(s("replace(\"a-b-c\", \"-\", \"+\")"), "a+b+c");
585 assert_eq!(i("length(chars(\"abc\"))"), 3);
586 }
587
588 #[test]
591 fn reverse_is_character_wise() {
592 assert_eq!(s("reverse(\"abc\")"), "cba");
593 assert_eq!(s("reverse(\"héllo\")"), "olléh", "must not corrupt the é");
594 }
595
596 #[test]
597 fn reverse_also_works_on_a_list() {
598 assert_eq!(s("join(reverse([1, 2, 3]), \",\")"), "3,2,1");
599 }
600
601 #[test]
606 fn to_int_is_nil_on_garbage_rather_than_zero() {
607 assert_eq!(i("to_int(\"42\")"), 42);
608 assert!(
609 matches!(eval("to_int(\"banana\")"), Value::Nil),
610 "Ruby would say 0 here; a falsy nil cannot be mistaken for a result"
611 );
612 assert!(
613 matches!(eval("to_int(\"0\")"), Value::Int(0)),
614 "and a real 0 is still a real 0 — the two must stay distinguishable"
615 );
616 }
617
618 #[test]
619 fn to_int_bang_raises_on_garbage() {
620 assert_eq!(i("to_int!(\"42\")"), 42);
621 let err = crate::run("to_int!(\"banana\")").expect_err("must raise");
622 assert!(err.to_string().contains("banana"), "must name it: {err}");
623 }
624
625 #[test]
626 fn numeric_conversions_and_abs() {
627 assert_eq!(i("to_int(3.9)"), 3);
628 assert_eq!(i("abs(0 - 5)"), 5);
629 assert!(matches!(eval("to_float(\"1.5\")"), Value::Float(_)));
630 assert!(matches!(eval("to_float(\"nope\")"), Value::Nil));
631 }
632
633 #[test]
635 fn a_non_string_argument_is_a_type_error() {
636 assert!(crate::run("upcase([1, 2])").is_err());
637 assert!(crate::run("join(\"not a list\", \",\")").is_err());
638 }
639}