1use tatara_lisp_eval::ffi::Arity;
36use tatara_lisp_eval::{EvalError, Interpreter, Value};
37
38const TRIM_OPTIONS: &[&str] = &["side", "chars"];
39
40fn trim_with(text: &str, opts: &Value, s: tatara_lisp::Span) -> Result<Value, EvalError> {
41 let bad = |message: String| crate::errors::raise_kind("bad_option", message, s);
42 let mut side = "both".to_string();
43 let mut chars: Option<Vec<char>> = None;
44 match opts {
45 Value::Nil => {}
46 Value::Map(m) => {
47 for (k, v) in m.iter() {
48 let key = match k {
49 tatara_lisp_eval::MapKey::Keyword(n)
50 | tatara_lisp_eval::MapKey::Symbol(n)
51 | tatara_lisp_eval::MapKey::Str(n) => n.to_string(),
52 _ => String::new(),
53 };
54 match (key.as_str(), v) {
55 ("side", Value::Keyword(k)) if matches!(&**k, "both" | "start" | "end") => {
56 side = k.to_string();
57 }
58 ("side", _) => return Err(bad("side must be :both, :start or :end".into())),
59 ("chars", Value::Str(c)) => chars = Some(c.chars().collect()),
60 ("chars", _) => return Err(bad("chars must be text".into())),
61 _ => {
62 return Err(bad(format!(
63 "unknown option {key}; this verb takes {}",
64 TRIM_OPTIONS.join(", ")
65 )))
66 }
67 }
68 }
69 }
70 other => {
71 return Err(bad(format!(
72 "options must be a map, got {}",
73 other.type_name()
74 )))
75 }
76 }
77 let out = match (&chars, side.as_str()) {
78 (None, "both") => text.trim(),
79 (None, "start") => text.trim_start(),
80 (None, _) => text.trim_end(),
81 (Some(set), "both") => text.trim_matches(|c| set.contains(&c)),
82 (Some(set), "start") => text.trim_start_matches(|c| set.contains(&c)),
83 (Some(set), _) => text.trim_end_matches(|c| set.contains(&c)),
84 };
85 Ok(Value::Str(out.into()))
86}
87
88fn as_str(v: &Value, span: tatara_lisp::Span) -> Result<String, EvalError> {
89 match v {
90 Value::Str(s) => Ok(s.to_string()),
91 Value::Symbol(s) | Value::Keyword(s) => Ok(s.to_string()),
94 other => Err(EvalError::type_mismatch(
95 "a string",
96 other.type_name(),
97 span,
98 )),
99 }
100}
101
102pub fn blue_equal(a: &Value, b: &Value) -> bool {
115 match (a, b) {
116 (Value::Nil, Value::Nil) => true,
117 (Value::Bool(x), Value::Bool(y)) => x == y,
118 (Value::Int(x), Value::Int(y)) => x == y,
119 (Value::Float(x), Value::Float(y)) => x == y,
120 (Value::Int(x), Value::Float(y)) | (Value::Float(y), Value::Int(x)) => (*x as f64) == *y,
121 (Value::Str(x), Value::Str(y))
122 | (Value::Symbol(x), Value::Symbol(y))
123 | (Value::Keyword(x), Value::Keyword(y)) => x == y,
124 (Value::List(x), Value::List(y)) => {
125 x.len() == y.len() && x.iter().zip(y.iter()).all(|(p, q)| blue_equal(p, q))
126 }
127 (Value::Map(x), Value::Map(y)) => {
128 x.len() == y.len()
129 && x.iter()
130 .all(|(k, v)| y.get(k).is_some_and(|w| blue_equal(v, w)))
131 }
132 (Value::Closure(x), Value::Closure(y)) => std::sync::Arc::ptr_eq(x, y),
133 (Value::NativeFn(x), Value::NativeFn(y)) => x.name == y.name,
134 (Value::Sexp(x, _), Value::Sexp(y, _)) => x == y,
135 _ => false,
136 }
137}
138
139fn chain_rules_value() -> Value {
144 use blue_lang_check::chains::{cases, Case, Kind, RULES};
145 let sym = |s: &str| Value::Symbol(std::sync::Arc::from(s));
146 let text = |s: &str| Value::Str(std::sync::Arc::from(s));
147 let hole = |n: usize| (0..n).map(|_| sym("x")).collect::<Vec<_>>();
148 let lambda = |body: Value| Value::list([sym("lambda"), Value::list([sym("x")]), body]);
149 let call = |name: &str, args: Vec<Value>| Value::list(std::iter::once(sym(name)).chain(args));
150 Value::list(RULES.iter().map(|rule| {
151 let rows = cases(rule).into_iter().map(|case| match case {
152 Case::Call { inner, argc } => {
153 let inner_call = call(inner, hole(argc));
154 Value::list([
155 text(&format!("{}({}(x))", rule.outer, inner)),
156 Value::Nil,
157 lambda(call(rule.outer, vec![inner_call.clone()])),
158 lambda(inner_call),
159 ])
160 }
161 Case::Literal(kind) => Value::list([
162 text(&format!("{}(<literal>)", rule.outer)),
163 Value::Keyword(std::sync::Arc::from(match kind {
164 Kind::Text => "text",
165 Kind::Int => "int",
166 Kind::Float => "float",
167 })),
168 lambda(call(rule.outer, vec![sym("x")])),
169 lambda(sym("x")),
170 ]),
171 });
172 Value::list([
173 text(rule.id),
174 text(rule.slug),
175 text(rule.outer),
176 text(rule.soundness),
177 Value::list(rows),
178 ])
179 }))
180}
181
182fn render(v: &Value) -> String {
183 match v {
184 Value::Nil => String::new(),
185 Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => s.to_string(),
186 Value::Error(e) => crate::errors::describe(e),
187 other => literal(other),
188 }
189}
190
191fn literal(v: &Value) -> String {
195 match v {
196 Value::Nil => "nil".into(),
197 Value::Bool(b) => b.to_string(),
198 Value::Int(n) => n.to_string(),
199 Value::Float(x) => float_text(*x),
200 Value::Str(s) => quoted(s),
201 Value::Symbol(s) => s.to_string(),
202 Value::Keyword(s) => format!(":{s}"),
203 Value::List(items) => {
204 let parts: Vec<String> = items.iter().map(literal).collect();
205 format!("[{}]", parts.join(", "))
206 }
207 Value::Map(m) => {
208 let mut entries: Vec<String> = m
209 .iter()
210 .map(|(k, v)| format!("{} {}", key_text(k), literal(v)))
211 .collect();
212 entries.sort();
213 format!("{{{}}}", entries.join(", "))
214 }
215 other => other.type_name().to_string(),
216 }
217}
218
219fn key_text(k: &tatara_lisp_eval::value::MapKey) -> String {
222 use tatara_lisp_eval::value::MapKey;
223 match k {
224 MapKey::Keyword(s) | MapKey::Symbol(s) => format!("{s}:"),
225 MapKey::Str(s) => format!("{} =>", quoted(s)),
226 MapKey::Int(n) => format!("{n} =>"),
227 MapKey::Float(bits) => format!("{} =>", float_text(f64::from_bits(*bits))),
228 MapKey::Bool(b) => format!("{b} =>"),
229 MapKey::Nil => "nil =>".into(),
230 }
231}
232
233fn float_text(x: f64) -> String {
235 if x.is_finite() && x.fract() == 0.0 && x.abs() < 1e16 {
236 format!("{x:.1}")
237 } else {
238 format!("{x}")
239 }
240}
241
242fn quoted(s: &str) -> String {
243 let mut out = String::with_capacity(s.len() + 2);
244 out.push('"');
245 for c in s.chars() {
246 match c {
247 '"' => out.push_str("\\\""),
248 '\\' => out.push_str("\\\\"),
249 '\n' => out.push_str("\\n"),
250 '\t' => out.push_str("\\t"),
251 c => out.push(c),
252 }
253 }
254 out.push('"');
255 out
256}
257
258fn list(items: Vec<Value>) -> Value {
259 Value::list(items)
260}
261
262pub fn install_blue_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
264 crate::errors::install(interp);
265 crate::paths::install(interp);
266 crate::spread::install(interp);
267 crate::collection::install(interp);
268 interp.register_fn(
273 "length",
274 Arity::Exact(1),
275 |a: &[Value], _h: &mut H, span| match &a[0] {
276 Value::List(items) => Ok(Value::Int(items.len() as i64)),
277 other => Ok(Value::Int(as_str(other, span)?.chars().count() as i64)),
278 },
279 );
280
281 interp.register_fn(
284 "builtin_names",
285 Arity::Exact(0),
286 |_a: &[Value], _h: &mut H, _s| {
287 Ok(Value::list(
288 crate::docs::NAMES
289 .iter()
290 .map(|n| Value::Str(std::sync::Arc::from(n.name))),
291 ))
292 },
293 );
294 interp.register_fn(
295 "chain_rules",
296 Arity::Exact(0),
297 |_a: &[Value], _h: &mut H, _s| Ok(chain_rules_value()),
298 );
299 interp.register_fn("to_s", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
300 Ok(Value::Str(render(&a[0]).into()))
301 });
302
303 interp.register_fn("upcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
304 Ok(Value::Str(as_str(&a[0], s)?.to_uppercase().into()))
305 });
306
307 interp.register_fn("downcase", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
308 Ok(Value::Str(as_str(&a[0], s)?.to_lowercase().into()))
309 });
310
311 interp.register_fn("trim", Arity::Range(1, 2), |a: &[Value], _h: &mut H, s| {
312 let text = as_str(&a[0], s)?;
313 trim_with(&text, a.get(1).unwrap_or(&Value::Nil), s)
314 });
315
316 interp.register_fn(
317 blue_lang_syntax::LOWERED_OPTIONAL_ARITY,
318 Arity::Exact(4),
319 |a: &[Value], _h: &mut H, s| {
320 let name = as_str(&a[0], s)?;
321 let (Value::List(rest), Value::Int(required), Value::Int(most)) = (&a[1], &a[2], &a[3])
322 else {
323 return Err(EvalError::native_fn(
324 blue_lang_syntax::LOWERED_OPTIONAL_ARITY,
325 "expects a name, the rest list and two counts",
326 s,
327 ));
328 };
329 if rest.len() as i64 > *most {
330 let want = Arity::Range(*required as usize, (*required + *most) as usize);
331 return Err(EvalError::native_fn(
332 name.as_str(),
333 format!(
334 "takes {}, got {}",
335 crate::messages::arguments(&want),
336 *required as usize + rest.len()
337 ),
338 s,
339 ));
340 }
341 Ok(Value::Nil)
342 },
343 );
344
345 interp.register_fn(
346 blue_lang_syntax::LOWERED_OPTIONAL_ARG,
347 Arity::Exact(2),
348 |a: &[Value], _h: &mut H, s| match (&a[0], &a[1]) {
349 (Value::List(rest), Value::Int(i)) => {
350 Ok(rest.get(*i as usize).cloned().unwrap_or(Value::Nil))
351 }
352 _ => Err(EvalError::native_fn(
353 blue_lang_syntax::LOWERED_OPTIONAL_ARG,
354 "expects the rest list and an index",
355 s,
356 )),
357 },
358 );
359
360 interp.register_fn(
367 "concat",
368 Arity::AtLeast(1),
369 |a: &[Value], _h: &mut H, _s| {
370 Ok(Value::Str(a.iter().map(render).collect::<String>().into()))
371 },
372 );
373
374 interp.register_fn("equal?", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
379 Ok(Value::Bool(blue_equal(&a[0], &a[1])))
380 });
381 interp.register_fn("not=", Arity::Exact(2), |a: &[Value], _h: &mut H, _s| {
382 Ok(Value::Bool(!blue_equal(&a[0], &a[1])))
383 });
384
385 interp.register_fn("nil?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
391 Ok(Value::Bool(matches!(a[0], Value::Nil)))
392 });
393 interp.register_fn("list?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
394 Ok(Value::Bool(matches!(a[0], Value::List(_))))
395 });
396 interp.register_fn("map?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
397 Ok(Value::Bool(matches!(a[0], Value::Map(_))))
398 });
399 interp.register_fn("float?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
400 Ok(Value::Bool(matches!(a[0], Value::Float(_))))
401 });
402 interp.register_fn("bool?", Arity::Exact(1), |a: &[Value], _h: &mut H, _s| {
403 Ok(Value::Bool(matches!(a[0], Value::Bool(_))))
404 });
405
406 interp.register_fn("cdr", Arity::Exact(1), |a: &[Value], _h: &mut H, span| {
411 match &a[0] {
412 Value::List(xs) if !xs.is_empty() => Ok(Value::List(std::sync::Arc::new(xs.skip(1)))),
414 Value::Nil | Value::List(_) => Err(EvalError::native_fn(
415 std::sync::Arc::<str>::from("cdr"),
416 "cdr of empty list",
417 span,
418 )),
419 other => Err(EvalError::type_mismatch("pair", other.type_name(), span)),
420 }
421 });
422 interp.register_owned_fn("append", Arity::Any, |a: Vec<Value>, _h: &mut H, span| {
423 let mut lists = Vec::with_capacity(a.len());
430 for v in a {
431 match v {
432 Value::Nil => {}
433 Value::List(xs) => lists.push(xs),
434 other => return Err(EvalError::type_mismatch("list", other.type_name(), span)),
435 }
436 }
437 let mut lists = lists.into_iter();
438 let Some(mut out) = lists.next() else {
439 return Ok(Value::List(std::sync::Arc::new(
440 tatara_lisp_eval::List::new(),
441 )));
442 };
443 let acc = std::sync::Arc::make_mut(&mut out);
444 for xs in lists {
445 acc.append(
446 std::sync::Arc::try_unwrap(xs)
447 .unwrap_or_else(|shared| tatara_lisp_eval::List::clone(&shared)),
448 );
449 }
450 Ok(Value::List(out))
451 });
452
453 interp.register_fn("split", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
454 let text = as_str(&a[0], s)?;
455 let sep = as_str(&a[1], s)?;
456 let parts: Vec<Value> = if sep.is_empty() {
465 text.chars()
466 .map(|c| Value::Str(c.to_string().into()))
467 .collect()
468 } else {
469 text.split(sep.as_str())
470 .map(|p| Value::Str(p.into()))
471 .collect()
472 };
473 Ok(list(parts))
474 });
475
476 interp.register_fn("join", Arity::Exact(2), |a: &[Value], _h: &mut H, s| {
477 let sep = as_str(&a[1], s)?;
478 match &a[0] {
479 Value::List(items) => Ok(Value::Str(
480 items
481 .iter()
482 .map(render)
483 .collect::<Vec<_>>()
484 .join(&sep)
485 .into(),
486 )),
487 other => Err(EvalError::type_mismatch("a list", other.type_name(), s).into()),
488 }
489 });
490
491 interp.register_fn(
492 "contains?",
493 Arity::Exact(2),
494 |a: &[Value], _h: &mut H, s| {
495 Ok(Value::Bool(as_str(&a[0], s)?.contains(&as_str(&a[1], s)?)))
496 },
497 );
498
499 interp.register_fn(
500 "starts_with?",
501 Arity::Exact(2),
502 |a: &[Value], _h: &mut H, s| {
503 Ok(Value::Bool(
504 as_str(&a[0], s)?.starts_with(&as_str(&a[1], s)?),
505 ))
506 },
507 );
508
509 interp.register_fn(
510 "ends_with?",
511 Arity::Exact(2),
512 |a: &[Value], _h: &mut H, s| {
513 Ok(Value::Bool(as_str(&a[0], s)?.ends_with(&as_str(&a[1], s)?)))
514 },
515 );
516
517 interp.register_fn(
518 "text_index",
519 Arity::Exact(3),
520 |a: &[Value], _h: &mut H, s| {
521 Ok(Value::Int(text_index(
522 &as_str(&a[0], s)?,
523 &as_str(&a[1], s)?,
524 as_index(&a[2], s)?,
525 )))
526 },
527 );
528
529 interp.register_fn(
530 "text_last_index",
531 Arity::Exact(3),
532 |a: &[Value], _h: &mut H, s| {
533 Ok(Value::Int(text_last_index(
534 &as_str(&a[0], s)?,
535 &as_str(&a[1], s)?,
536 as_index(&a[2], s)?,
537 )))
538 },
539 );
540
541 interp.register_fn("replace", Arity::Exact(3), |a: &[Value], _h: &mut H, s| {
542 Ok(Value::Str(
543 as_str(&a[0], s)?
544 .replace(&as_str(&a[1], s)?, &as_str(&a[2], s)?)
545 .into(),
546 ))
547 });
548
549 interp.register_fn("reverse", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
550 match &a[0] {
551 Value::List(items) => Ok(Value::list(items.iter().rev().cloned())),
552 other => Ok(Value::Str(
555 as_str(other, s)?.chars().rev().collect::<String>().into(),
556 )),
557 }
558 });
559
560 interp.register_fn("chars", Arity::Exact(1), |a: &[Value], _h: &mut H, s| {
561 Ok(list(
562 as_str(&a[0], s)?
563 .chars()
564 .map(|c| Value::Str(c.to_string().into()))
565 .collect(),
566 ))
567 });
568
569 interp.register_fn(
570 "text_bytes",
571 Arity::Exact(1),
572 |a: &[Value], _h: &mut H, s| {
573 Ok(list(
574 as_str(&a[0], s)?
575 .as_bytes()
576 .iter()
577 .map(|b| Value::Int(i64::from(*b)))
578 .collect(),
579 ))
580 },
581 );
582
583 interp.register_fn(
584 "bytes_text",
585 Arity::Exact(1),
586 |a: &[Value], _h: &mut H, s| {
587 let bytes = crate::crypto::message_bytes(&a[0])
588 .map_err(|e| EvalError::native_fn("bytes_text", e.to_string(), s))?;
589 Ok(match String::from_utf8(bytes) {
590 Ok(text) => Value::Str(text.into()),
591 Err(_) => Value::Nil,
592 })
593 },
594 );
595
596 interp.register_fn(
603 "to_int",
604 Arity::Exact(1),
605 |a: &[Value], _h: &mut H, s| match &a[0] {
606 Value::Int(n) => Ok(Value::Int(*n)),
607 Value::Float(x) => Ok(float_to_int(*x).map_or(Value::Nil, Value::Int)),
611 other => Ok(as_str(other, s)?
612 .trim()
613 .parse::<i64>()
614 .map_or(Value::Nil, Value::Int)),
615 },
616 );
617
618 interp.register_fn(
619 "to_int!",
620 Arity::Exact(1),
621 |a: &[Value], _h: &mut H, s| match &a[0] {
622 Value::Int(n) => Ok(Value::Int(*n)),
623 Value::Float(x) => float_to_int(*x)
624 .map(Value::Int)
625 .ok_or(EvalError::IntegerOverflow {
626 op: "to_int!",
627 at: s,
628 }),
629 other => {
630 let text = as_str(other, s)?;
631 text.trim().parse::<i64>().map(Value::Int).map_err(|_| {
632 EvalError::native_fn(
633 "to_int!",
634 "`".to_string() + &text + "` is not an integer",
635 s,
636 )
637 .into()
638 })
639 }
640 },
641 );
642
643 interp.register_fn(
644 "to_float",
645 Arity::Exact(1),
646 |a: &[Value], _h: &mut H, s| match &a[0] {
647 Value::Float(x) => Ok(Value::Float(*x)),
648 Value::Int(n) => Ok(Value::Float(*n as f64)),
649 other => Ok(as_str(other, s)?
650 .trim()
651 .parse::<f64>()
652 .map_or(Value::Nil, Value::Float)),
653 },
654 );
655
656 interp.register_fn(
657 "abs",
658 Arity::Exact(1),
659 |a: &[Value], _h: &mut H, s| match &a[0] {
660 Value::Int(n) => n
663 .checked_abs()
664 .map(Value::Int)
665 .ok_or(EvalError::IntegerOverflow { op: "abs", at: s }),
666 Value::Float(x) => Ok(Value::Float(x.abs())),
667 other => Err(EvalError::type_mismatch("a number", other.type_name(), s).into()),
668 },
669 );
670
671 interp.register_fn("range", Arity::Range(1, 3), |a: &[Value], _h: &mut H, s| {
692 let (start, end, step) = match a.len() {
693 1 => (Value::Int(0), a[0].clone(), Value::Int(1)),
694 2 => (a[0].clone(), a[1].clone(), Value::Int(1)),
695 _ => (a[0].clone(), a[1].clone(), a[2].clone()),
696 };
697 range_list(start, &end, &step, s)
698 });
699
700 interp.register_fn(
707 "write_stdout",
708 Arity::Exact(1),
709 |a: &[Value], _h: &mut H, s| {
710 use std::io::Write;
711 let text = as_str(&a[0], s)?;
712 let mut out = std::io::stdout().lock();
713 out.write_all(text.as_bytes())
714 .and_then(|()| out.flush())
715 .map_err(|e| EvalError::native_fn("write_stdout", e.to_string(), s))?;
716 Ok(Value::Nil)
717 },
718 );
719 interp.register_fn(
720 "write_stderr",
721 Arity::Exact(1),
722 |a: &[Value], _h: &mut H, s| {
723 use std::io::Write;
724 let text = as_str(&a[0], s)?;
725 let mut err = std::io::stderr().lock();
726 err.write_all(text.as_bytes())
727 .and_then(|()| err.flush())
728 .map_err(|e| EvalError::native_fn("write_stderr", e.to_string(), s))?;
729 Ok(Value::Nil)
730 },
731 );
732
733 interp.register_higher_order_fn(
740 "sort_keyed",
741 Arity::Exact(2),
742 |a: &[Value], host: &mut H, caller: &tatara_lisp_eval::ffi::Caller<H>, s| {
743 let xs = match &a[1] {
744 Value::List(xs) => xs.to_vec(),
745 Value::Nil => Vec::new(),
746 other => {
747 return Err(EvalError::type_mismatch("a list", other.type_name(), s));
748 }
749 };
750 let mut keyed = Vec::with_capacity(xs.len());
751 for x in xs {
752 keyed.push((caller.apply_value(&a[0], vec![x.clone()], host, s)?, x));
753 }
754 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
755 },
756 );
757
758 interp.register_fn(
759 "error_message",
760 Arity::Exact(1),
761 |a: &[Value], _h: &mut H, s| match &a[0] {
762 Value::Error(e) => Ok(Value::Str(e.message.clone())),
763 other => Err(EvalError::type_mismatch("an error", other.type_name(), s)),
764 },
765 );
766
767 interp.register_fn(
768 "error_kind",
769 Arity::Exact(1),
770 |a: &[Value], _h: &mut H, s| match &a[0] {
771 Value::Error(e) => Ok(Value::Keyword(e.tag.clone())),
772 other => Err(EvalError::type_mismatch("an error", other.type_name(), s)),
773 },
774 );
775
776 interp.register_fn(
777 "map_entries",
778 Arity::Exact(1),
779 |a: &[Value], _h: &mut H, s| match &a[0] {
780 Value::Map(m) => {
781 let keyed = m
782 .iter()
783 .map(|(k, v)| (k.to_value(), list(vec![k.to_value(), v.clone()])))
784 .collect();
785 Ok(list(tatara_lisp_eval::hof::sort_keyed_values(keyed, s)?))
786 }
787 Value::Nil => Ok(list(Vec::new())),
788 other => Err(EvalError::type_mismatch("a map", other.type_name(), s)),
789 },
790 );
791}
792
793fn as_index(v: &Value, span: tatara_lisp::Span) -> Result<i64, EvalError> {
794 match v {
795 Value::Int(n) => Ok(*n),
796 other => Err(EvalError::type_mismatch(
797 "an integer",
798 other.type_name(),
799 span,
800 )),
801 }
802}
803
804fn text_index(text: &str, sub: &str, from: i64) -> i64 {
805 let from = usize::try_from(from.max(0)).unwrap_or(usize::MAX);
806 let start = match text.char_indices().nth(from) {
807 Some((b, _)) => b,
808 None if text.chars().count() == from => text.len(),
809 None => return -1,
810 };
811 text[start..].find(sub).map_or(-1, |b| {
812 i64::try_from(from + text[start..start + b].chars().count()).unwrap_or(i64::MAX)
813 })
814}
815
816fn text_last_index(text: &str, sub: &str, from: i64) -> i64 {
817 let Ok(from) = usize::try_from(from) else {
818 return -1;
819 };
820 let total = text.chars().count();
821 let width = sub.chars().count();
822 if width > total {
823 return -1;
824 }
825 let limit = from.min(total - width);
826 let end = text
827 .char_indices()
828 .nth(limit + width)
829 .map_or(text.len(), |(b, _)| b);
830 text[..end].rfind(sub).map_or(-1, |b| {
831 i64::try_from(text[..b].chars().count()).unwrap_or(i64::MAX)
832 })
833}
834
835fn range_num(v: &Value, s: tatara_lisp::Span) -> Result<f64, EvalError> {
837 match v {
838 Value::Int(n) => Ok(*n as f64),
839 Value::Float(x) => Ok(*x),
840 other => Err(EvalError::native_fn(
841 "range",
842 "expected a number, got ".to_string() + other.type_name(),
843 s,
844 )),
845 }
846}
847
848fn float_to_int(x: f64) -> Option<i64> {
851 const LIMIT: f64 = 9_223_372_036_854_775_808.0;
852 let t = x.trunc();
853 #[allow(clippy::cast_possible_truncation)]
854 (t.is_finite() && (-LIMIT..LIMIT).contains(&t)).then(|| t as i64)
855}
856
857fn range_add(a: &Value, b: &Value, s: tatara_lisp::Span) -> Result<Value, EvalError> {
860 match (a, b) {
861 (Value::Int(x), Value::Int(y)) => x.checked_add(*y).map(Value::Int).ok_or_else(|| {
862 EvalError::native_fn("range", "an element overflowed a 64-bit integer", s)
863 }),
864 _ => Ok(Value::Float(range_num(a, s)? + range_num(b, s)?)),
865 }
866}
867
868fn range_list(
869 start: Value,
870 end: &Value,
871 step: &Value,
872 s: tatara_lisp::Span,
873) -> Result<Value, EvalError> {
874 let end_f = range_num(end, s)?;
875 let step_f = range_num(step, s)?;
876 range_num(&start, s)?;
877 if step_f == 0.0 {
878 return Err(EvalError::native_fn(
879 "range",
880 "a step of zero never reaches the end",
881 s,
882 ));
883 }
884 let mut out = Vec::new();
885 let mut cur = start;
886 loop {
887 let c = range_num(&cur, s)?;
888 if (step_f > 0.0 && c >= end_f) || (step_f < 0.0 && c <= end_f) {
889 break;
890 }
891 let next = range_add(&cur, step, s)?;
892 out.push(cur);
893 cur = next;
894 }
895 Ok(list(out))
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901
902 #[test]
903 fn sort_keyed_is_stable_keyed_and_deep() {
904 assert_eq!(
906 ints("map(fn(p) last(p) end, sort_keyed(fn(p) first(p) end, [[2, 1], [1, 2], [2, 3], [1, 4]]))"),
907 vec![2, 4, 1, 3]
908 );
909 assert_eq!(
911 i("first(sort_keyed(fn(x) x end, reverse(range(0, 20000))))"),
912 0
913 );
914 assert_eq!(i("last(sort_keyed(fn(x) 0 - x end, range(0, 20000)))"), 0);
915 assert_eq!(ints("sort_keyed(fn(x) x end, [])"), Vec::<i64>::new());
917 assert!(crate::run("sort_keyed(fn(x) x end, [1, \"a\"])").is_err());
918 }
919
920 fn eval(src: &str) -> Value {
921 crate::run(src)
922 .unwrap_or_else(|e| panic!("{src:?}: {e}"))
923 .value
924 }
925
926 fn s(src: &str) -> String {
927 match eval(src) {
928 Value::Str(v) => v.to_string(),
929 other => panic!("{src:?} produced {other:?}"),
930 }
931 }
932
933 fn i(src: &str) -> i64 {
934 match eval(src) {
935 Value::Int(v) => v,
936 other => panic!("{src:?} produced {other:?}"),
937 }
938 }
939
940 fn ints(src: &str) -> Vec<i64> {
941 match eval(src) {
942 Value::List(xs) => xs
943 .iter()
944 .map(|v| match v {
945 Value::Int(n) => *n,
946 other => panic!("{src:?} produced a non-Int element {other:?}"),
947 })
948 .collect(),
949 other => panic!("{src:?} produced {other:?}"),
950 }
951 }
952
953 #[test]
956 fn range_answers_as_the_lisp_definition_did() {
957 assert_eq!(ints("range(5)"), vec![0, 1, 2, 3, 4]);
958 assert_eq!(ints("range(2, 6)"), vec![2, 3, 4, 5]);
959 assert_eq!(ints("range(0, 10, 2)"), vec![0, 2, 4, 6, 8]);
960 assert_eq!(ints("range(10, 0, 0 - 2)"), vec![10, 8, 6, 4, 2]);
961 assert!(ints("range(5, 5)").is_empty());
962 assert!(ints("range(5, 2)").is_empty());
963 assert!(ints("range(0)").is_empty());
964 match eval("range(0, 1, 0.5)") {
966 Value::List(xs) => {
967 assert!(matches!(xs[0], Value::Int(0)), "{xs:?}");
968 assert!(matches!(xs[1], Value::Float(x) if x == 0.5), "{xs:?}");
969 assert_eq!(xs.len(), 2);
970 }
971 other => panic!("{other:?}"),
972 }
973 }
974
975 #[test]
981 fn range_builds_long_lists_without_recursing() {
982 assert_eq!(i("length(range(0, 200000))"), 200_000);
983 assert_eq!(i("nth(199999, range(0, 200000))"), 199_999);
984 }
985
986 #[test]
989 fn range_refuses_a_zero_step_and_a_non_number() {
990 assert!(crate::run("range(0, 5, 0)").is_err());
991 assert!(crate::run("range(0, \"x\")").is_err());
992 }
993
994 #[test]
998 fn length_counts_characters_not_bytes() {
999 assert_eq!(i("length(\"hello\")"), 5);
1000 assert_eq!(i("length(\"héllo\")"), 5, "must not be 6");
1002 assert_eq!(i("length(\"😀\")"), 1, "must not be 4");
1004 }
1005
1006 #[test]
1009 fn a_combining_sequence_counts_scalars_not_graphemes() {
1010 assert_eq!(
1012 i("length(\"e\\u{301}\")"),
1013 2,
1014 "blue counts scalar values; Elixir's String.length would say 1"
1015 );
1016 }
1017
1018 #[test]
1019 fn length_also_works_on_a_list() {
1020 assert_eq!(i("length([1, 2, 3])"), 3);
1021 }
1022
1023 #[test]
1024 fn case_and_trim() {
1025 assert_eq!(s("upcase(\"abc\")"), "ABC");
1026 assert_eq!(s("downcase(\"ABC\")"), "abc");
1027 assert_eq!(s("trim(\" hi \")"), "hi");
1028 assert_eq!(s("upcase(\"é\")"), "É");
1030 }
1031
1032 #[test]
1033 fn concat_and_to_s() {
1034 assert_eq!(s("concat(\"a\", \"b\")"), "ab");
1035 assert_eq!(s("concat(\"n=\", 42)"), "n=42");
1036 assert_eq!(s("to_s(42)"), "42");
1037 assert_eq!(s("to_s(true)"), "true");
1038 }
1039
1040 #[test]
1044 fn plus_is_not_string_concatenation() {
1045 assert!(
1046 crate::run("\"a\" + \"b\"").is_err(),
1047 "`+` must not silently concatenate — use concat"
1048 );
1049 }
1050
1051 #[test]
1052 fn split_and_join() {
1053 assert_eq!(i("length(split(\"a,b,c\", \",\"))"), 3);
1054 assert_eq!(s("join(split(\"a,b,c\", \",\"), \"-\")"), "a-b-c");
1055 assert_eq!(i("length(split(\"abc\", \"\"))"), 3);
1057 }
1058
1059 #[test]
1060 fn predicates() {
1061 assert!(matches!(
1062 eval("contains?(\"hello\", \"ell\")"),
1063 Value::Bool(true)
1064 ));
1065 assert!(matches!(
1066 eval("contains?(\"hello\", \"xyz\")"),
1067 Value::Bool(false)
1068 ));
1069 assert!(matches!(
1070 eval("starts_with?(\"hello\", \"he\")"),
1071 Value::Bool(true)
1072 ));
1073 assert!(matches!(
1074 eval("ends_with?(\"hello\", \"lo\")"),
1075 Value::Bool(true)
1076 ));
1077 }
1078
1079 fn refused(src: &str) -> String {
1080 match crate::run(src) {
1081 Ok(v) => panic!("{src:?} should fail, produced {:?}", v.value),
1082 Err(e) => e.to_string(),
1083 }
1084 }
1085
1086 #[test]
1087 fn text_bytes_and_bytes_text_are_utf8_both_ways() {
1088 assert_eq!(ints("text_bytes(\"abc\")"), vec![97, 98, 99]);
1089 assert_eq!(ints("text_bytes(\"é\")"), vec![195, 169]);
1090 assert!(ints("text_bytes(\"\")").is_empty());
1091 assert_eq!(s("bytes_text([104, 195, 169])"), "hé");
1092 assert_eq!(s("bytes_text(text_bytes(\"€ x\"))"), "€ x");
1093 assert_eq!(s("bytes_text([])"), "");
1094 assert!(matches!(eval("bytes_text([195])"), Value::Nil));
1095 assert!(matches!(eval("bytes_text([255, 97])"), Value::Nil));
1096 assert!(refused("bytes_text([256])").contains("outside 0..=255"));
1097 assert!(refused("bytes_text(nil)").contains("string or a list of bytes"));
1098 assert!(refused("text_bytes(5)").contains("string"));
1099 }
1100
1101 #[test]
1102 fn trim_without_options_cuts_whitespace_at_both_ends() {
1103 assert_eq!(s("trim(\" a b \\n\")"), "a b");
1104 assert_eq!(s("trim(\" a \", nil)"), "a");
1105 assert_eq!(s("trim(\" a \", {})"), "a");
1106 assert_eq!(s("trim(\"\")"), "");
1107 assert_eq!(s("trim(\" \")"), "");
1108 assert_eq!(s("trim(\"\", {side: :start})"), "");
1109 }
1110
1111 #[test]
1112 fn trim_side_keeps_the_other_end() {
1113 assert_eq!(s("trim(\" a \", {side: :both})"), "a");
1114 assert_eq!(s("trim(\" a \", {side: :start})"), "a ");
1115 assert_eq!(s("trim(\" a \", {side: :end})"), " a");
1116 }
1117
1118 #[test]
1119 fn trim_chars_replaces_whitespace_as_the_set_to_cut() {
1120 assert_eq!(s("trim(\"--a-b--\", {chars: \"-\"})"), "a-b");
1121 assert_eq!(s("trim(\"-+a+-\", {chars: \"-+\", side: :start})"), "a+-");
1122 assert_eq!(s("trim(\"-+a+-\", {chars: \"-+\", side: :end})"), "-+a");
1123 assert_eq!(s("trim(\" a \", {chars: \"-\"})"), " a ");
1124 assert_eq!(s("trim(\"aaa\", {chars: \"a\"})"), "");
1125 assert_eq!(s("trim(\"héé\", {chars: \"é\", side: :end})"), "h");
1126 assert_eq!(s("trim(\" a \", {chars: \"\"})"), " a ");
1127 }
1128
1129 #[test]
1130 fn trim_refuses_what_it_does_not_take() {
1131 assert!(refused("trim(\"a\", {sid: :end})").contains("bad_option"));
1132 assert!(refused("trim(\"a\", {sid: :end})").contains("side, chars"));
1133 assert!(refused("trim(\"a\", {side: :middle})").contains("side must be"));
1134 assert!(refused("trim(\"a\", {side: \"end\"})").contains("bad_option"));
1135 assert!(refused("trim(\"a\", {chars: 1})").contains("chars must be text"));
1136 assert!(refused("trim(\"a\", 5)").contains("options must be a map"));
1137 assert!(refused("trim(\"a\", {}, {})").contains("1 to 2"));
1138 assert!(refused("trim()").contains("1 to 2"));
1139 assert!(refused("trim(5)").contains("string"));
1140 }
1141
1142 #[test]
1143 fn trim_is_still_a_value_other_functions_take() {
1144 assert_eq!(s("join(map(trim, [\" a\", \"b \"]), \",\")"), "a,b");
1145 }
1146
1147 #[test]
1148 fn text_index_answers_character_positions() {
1149 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 0)"), 2);
1150 assert_eq!(i("text_index(\"héllo héllo\", \"llo\", 3)"), 8);
1151 assert_eq!(i("text_index(\"héllo\", \"x\", 0)"), -1);
1152 assert_eq!(i("text_index(\"abc\", \"\", 3)"), 3);
1153 assert_eq!(i("text_index(\"abc\", \"\", 4)"), -1);
1154 assert_eq!(i("text_index(\"abc\", \"a\", -5)"), 0);
1155 assert_eq!(i("text_index(\"\", \"a\", 0)"), -1);
1156 }
1157
1158 #[test]
1159 fn text_last_index_finds_the_last_start_at_or_before_from() {
1160 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 99)"), 8);
1161 assert_eq!(i("text_last_index(\"héllo héllo\", \"llo\", 7)"), 2);
1162 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 1)"), 1);
1163 assert_eq!(i("text_last_index(\"aaa\", \"aa\", 0)"), 0);
1164 assert_eq!(i("text_last_index(\"abc\", \"x\", 2)"), -1);
1165 assert_eq!(i("text_last_index(\"abc\", \"a\", -1)"), -1);
1166 assert_eq!(i("text_last_index(\"ab\", \"abc\", 0)"), -1);
1167 assert_eq!(i("text_last_index(\"abc\", \"\", 9)"), 3);
1168 }
1169
1170 #[test]
1171 fn replace_and_chars() {
1172 assert_eq!(s("replace(\"a-b-c\", \"-\", \"+\")"), "a+b+c");
1173 assert_eq!(i("length(chars(\"abc\"))"), 3);
1174 }
1175
1176 #[test]
1179 fn reverse_is_character_wise() {
1180 assert_eq!(s("reverse(\"abc\")"), "cba");
1181 assert_eq!(s("reverse(\"héllo\")"), "olléh", "must not corrupt the é");
1182 }
1183
1184 #[test]
1185 fn reverse_also_works_on_a_list() {
1186 assert_eq!(s("join(reverse([1, 2, 3]), \",\")"), "3,2,1");
1187 }
1188
1189 #[test]
1194 fn to_int_is_nil_on_garbage_rather_than_zero() {
1195 assert_eq!(i("to_int(\"42\")"), 42);
1196 assert!(
1197 matches!(eval("to_int(\"banana\")"), Value::Nil),
1198 "Ruby would say 0 here; a falsy nil cannot be mistaken for a result"
1199 );
1200 assert!(
1201 matches!(eval("to_int(\"0\")"), Value::Int(0)),
1202 "and a real 0 is still a real 0 — the two must stay distinguishable"
1203 );
1204 }
1205
1206 #[test]
1207 fn to_int_bang_raises_on_garbage() {
1208 assert_eq!(i("to_int!(\"42\")"), 42);
1209 let err = crate::run("to_int!(\"banana\")").expect_err("must raise");
1210 assert!(err.to_string().contains("banana"), "must name it: {err}");
1211 }
1212
1213 #[test]
1214 fn numeric_conversions_and_abs() {
1215 assert_eq!(i("to_int(3.9)"), 3);
1216 assert_eq!(i("abs(0 - 5)"), 5);
1217 assert!(matches!(eval("to_float(\"1.5\")"), Value::Float(_)));
1218 assert!(matches!(eval("to_float(\"nope\")"), Value::Nil));
1219 }
1220
1221 #[test]
1223 fn a_non_string_argument_is_a_type_error() {
1224 assert!(crate::run("upcase([1, 2])").is_err());
1225 assert!(crate::run("join(\"not a list\", \",\")").is_err());
1226 }
1227}