Skip to main content

blue_lang_runtime/
stdlib.rs

1//! blue's string and number core.
2//!
3//! tatara-lisp ships arithmetic, comparison, list and trig primitives and
4//! **no string operations at all** — no length, no concatenation, no case
5//! conversion, no number parsing. For a language whose surface is Ruby's, that
6//! is the largest parity gap there is: `String` is the most-used type in Ruby
7//! by a wide margin.
8//!
9//! # Why these live in blue and not in tatara-lisp
10//!
11//! The fleet rule is to extend the substrate rather than re-implement, and
12//! generic helpers belong upstream. These are not generic: the *semantics* are
13//! blue's, and they are Ruby's semantics specifically.
14//!
15//! The clearest case is `length`. Ruby's `String#length` counts **characters**;
16//! Rust's `str::len` counts **bytes**; Elixir's `String.length/1` counts
17//! grapheme clusters. Three languages, three answers, all defensible. Blue owes
18//! its users Ruby's answer, and encoding that choice into tatara-lisp would push
19//! one language's convention onto every other consumer of the substrate.
20//!
21//! Promoting a genuinely encoding-neutral core upstream later stays open; the
22//! character-counting ones are blue's by right.
23//!
24//! # Character, not byte, not grapheme
25//!
26//! Every index and length here is in **Unicode scalar values** (Rust `char`).
27//! That matches Ruby for the overwhelming majority of text and is stated rather
28//! than left to be discovered — a `length` that silently returns bytes is the
29//! bug that only appears once a user types a non-ASCII character.
30//!
31//! Grapheme clusters (Elixir's choice) would need a segmentation table; where
32//! the two differ — a family emoji, a combining accent — blue reports scalar
33//! values. `a_combining_sequence_counts_scalars_not_graphemes` pins it.
34
35use 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        // A symbol is text the author wrote; accepting it makes `upcase(:ok)`
42        // work the way a Ruby programmer expects of a symbol.
43        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
52/// blue's one equality — what `==`, `!=`, `contains`, `index_of`, `count_of`
53/// and `distinct` all mean (okite D0001–D0003).
54///
55/// Ruby's answer: numbers compare by value across Int and Float (`4 == 4.0`),
56/// strings, symbols and keywords by text (each only with its own kind), lists
57/// element by element, maps by their entries whatever order they were built in,
58/// nil only with nil (not with `[]`, D0004), functions by identity. Until
59/// 2026-09-27 `==` was tatara's Scheme `equal?`, under which `4 == 4.0` was
60/// false and two equal maps were unequal unless they were the same object.
61/// tatara keeps Scheme's `equal?` for its own programs; blue redefines the
62/// name in its interpreter, and tatara's own `member?`, `position` and
63/// `distinct` call `equal?` by name, so they follow.
64pub 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
89/// `to_s` (okite D0005, D0006): text for a person. A string, symbol or keyword
90/// is its own text and nil is empty, as in Ruby; every other value is its blue
91/// literal. Until 2026-09-27 a map rendered as the word `map`, a list lost its
92/// brackets (`[1, {a: 2}]` came out `1 map`) and `1.0` came out `1`.
93fn 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
141/// A value as blue source writes it: `[1, "a", nil]`, `{a: 1, b: 2.0}`.
142/// Map entries are sorted by key, since a map has no order of its own and the
143/// text must be the same for equal maps (D0006).
144fn 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
169/// A map key as it is written before its value: `a:` for a keyword or symbol,
170/// `"a" =>` / `1 =>` for anything else.
171fn 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
183/// A float keeps its point, as in Ruby: `1.0`, `2.5`.
184fn 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
212/// Install blue's string and number core.
213pub 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    // ── text ──────────────────────────────────────────────────────────
218
219    // `length` counts CHARACTERS, per Ruby. Also accepts a list, where it is
220    // the element count — Ruby's `Array#length`.
221    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    // The documented builtins, for a program that reasons about names (the
231    // catalogue's "shares a builtin's name" column).
232    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    // `concat(a, …)` joins the text of one or more arguments, left to right
265    // (okite D0007; Ruby's String#concat takes many). Two-only until
266    // 2026-09-27, which made `concat(a, b, c)` an arity error. `+` stays
267    // arithmetic: Ruby overloads `+` on String, but blue's `+` lowers to
268    // tatara's numeric `+`, and making it polymorphic would make a type error
269    // at a seam disappear into a string.
270    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    // ── equality and kinds (okite D0001–D0004, D0010) ─────────────────────
279    //
280    // `==` lowers to `equal?` and `!=` to `not=`; both are redefined here so
281    // blue's interpreter has one equality. See `blue_equal`.
282    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    // One predicate per kind, and exactly one holds of any value (D0010).
290    // tatara's `nil?` is its Scheme alias for `null?`, true of `[]` as well,
291    // and its `list?` is true of nil; blue's nil is absent and `[]` is an empty
292    // list (D0004), so blue redefines both. tatara's own library ends its
293    // recursion with `null?`, which is untouched.
294    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    // The empty list is always `[]`, never nil (D0011). tatara's `cdr` (and so
311    // `rest`, which is `cdr`) and `append` returned nil for an empty result,
312    // Scheme's convention, which under D0004 made `rest([1]) == []` false.
313    // Same contracts otherwise: `cdr` of an empty list is still an error.
314    interp.register_fn("cdr", Arity::Exact(1), |a: &[Value], _h: &mut H, span| {
315        match &a[0] {
316            // Shares the tail's structure: O(log n), not a copy of it.
317            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        // Persistent concatenation: each operand's structure is shared, so
328        // building a list by `append(acc, [x])` is linear overall, not
329        // quadratic (G7). The arguments arrive by value, so an accumulator
330        // the caller holds alone (tatara-lisp-eval runs a tail call to an
331        // owned primitive after releasing the caller's frame) is extended in
332        // its own allocation by `Arc::make_mut` instead of copied at the tail.
333        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        // An empty separator splits into characters, as Ruby's `split("")`
361        // does. Rust's `split("")` yields leading/trailing empties instead,
362        // which is the wrong answer here.
363        // Every field is kept: n separators give n + 1 fields, so `join` undoes
364        // `split` exactly (okite D0012). A deliberate deviation from Ruby, which
365        // drops trailing empty fields: tried on 2026-09-27, it broke nisshi's
366        // hash chain, kueri's scripts and moji's `lines`, because a parser whose
367        // split loses a field loses data and still reports success.
368        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            // Reversed by CHARACTER, so a multi-byte character survives. A
457            // byte-wise reverse produces invalid UTF-8.
458            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    // ── numbers ───────────────────────────────────────────────────────
474
475    // `to_int` RETURNS NIL on unparseable input rather than raising. Ruby's
476    // `String#to_i` answers 0 for garbage, which silently turns a parse failure
477    // into a plausible number; nil is falsy and cannot be mistaken for a
478    // result. `to_int!` is the raising form for callers who want the failure.
479    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            // A float with no Int (non-finite, or past i64) is nil like any
485            // other unconvertible input; `as i64` saturated it to a plausible
486            // wrong number.
487            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            // Checked, as tatara's `abs` is (G5): |i64::MIN| does not fit, and
538            // `n.abs()` wrapped in release and panicked in debug.
539            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    // ── ranges ────────────────────────────────────────────────────────
549
550    // `range` is a LOOP here, not a recursion. tatara-lisp's stdlib defines it
551    // in Lisp (`lisp_stdlib.tlisp`, where `range-impl` conses one frame per
552    // element), so `range(0, 5000)` overflowed the 8 MiB main stack and
553    // aborted the process. Everything built on it inherited the ceiling:
554    // retsu's `indexes`, `zip_with` and `enumerate`, and shomei's
555    // `chain_first_break`, which could not verify a 5,000-entry chain
556    // (measured 2026-09-24: 4,000 elements fine, 5,000 aborted).
557    //
558    // Same arities and answers as the Lisp definition: (end), (start, end),
559    // (start, end, step), ascending for a positive step, descending for a
560    // negative one, and each next element is the previous plus the step under
561    // the ordinary number rules (Int + Int is Int; a Float anywhere makes the
562    // rest Float). Two refusals the Lisp version lacked: a zero step, which
563    // recursed forever, and a wrong arity, which printed a message and
564    // returned nil.
565    //
566    // The destination is upstream: when tatara-lisp's own `range` is a loop,
567    // this registration is deleted.
568    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    // `write_stdout(s)` / `write_stderr(s)`: the text exactly, no quotes, no
578    // newline added. `print`, `println` and `display` all render a string
579    // with its quotes (a value printer, right for a REPL and wrong for a
580    // command's output), so until these a blue command-line tool could not
581    // print plain text. Output only, like `println`; anything else is not a
582    // string and is refused.
583    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    // `sort_keyed(key, xs)`: xs ordered by key(x), stably, in O(n log n),
611    // under `compare`'s rules. Every sort written in blue (junjo's) recurses
612    // per element and aborts the process near 600 elements; this is
613    // tatara-lisp's `sort-by-key` (0.3.60), which blue cannot name because
614    // it is kebab-case, bound to a name blue can. The engine is upstream,
615    // `hof::sort_keyed_values`; only the spelling lives here.
616    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
712/// A number's value as a float, or a type error naming `range`.
713fn 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
725/// A float's integer part as an `i64`, or `None` when it is not finite or
726/// does not fit. `as i64` saturates instead.
727fn 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
734/// `a + b` under the number rules the Lisp `range` used: Int + Int stays Int,
735/// anything with a Float is Float.
736fn 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        // Ties keep input order; the key is computed per element.
782        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        // 20,000 in reverse: every blue-written sort aborts far below this.
787        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        // The empty case, and a refusal for keys that cannot be ordered.
793        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    /// **`range` gives the Lisp definition's answers**, arity by arity,
831    /// including the descending form and the empty cases.
832    #[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        // A float step makes the elements after the first floats, as `+` did.
842        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    /// **`range` is a loop, not a recursion.** The Lisp definition consed one
853    /// stack frame per element and aborted the process near 5,000 elements on
854    /// an 8 MiB stack. This runs on the test harness's 2 MiB thread, where the
855    /// Lisp one died sooner still. Red run, 2026-09-24: with the registration
856    /// removed, this test aborted the test binary with a stack overflow.
857    #[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    /// A zero step never reaches the end: refused, where the Lisp definition
864    /// recursed until the stack gave out. A non-number is a named error.
865    #[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    /// **`length` counts CHARACTERS, as Ruby does — not bytes.** A `length`
872    /// that returns bytes is the bug that only surfaces once a user types a
873    /// non-ASCII character, which is exactly when it is hardest to trace.
874    #[test]
875    fn length_counts_characters_not_bytes() {
876        assert_eq!(i("length(\"hello\")"), 5);
877        // "héllo" is 6 bytes, 5 characters.
878        assert_eq!(i("length(\"héllo\")"), 5, "must not be 6");
879        // An emoji is 4 bytes, 1 character.
880        assert_eq!(i("length(\"😀\")"), 1, "must not be 4");
881    }
882
883    /// blue reports **scalar values**, not grapheme clusters. Elixir would say
884    /// 1 here; blue says 2. Stated and pinned rather than left to surprise.
885    #[test]
886    fn a_combining_sequence_counts_scalars_not_graphemes() {
887        // "e" + U+0301 COMBINING ACUTE — one grapheme, two scalars.
888        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        // Non-ASCII case works, which a byte-wise implementation would botch.
906        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    /// **`+` stays arithmetic.** Ruby overloads it on String, but blue's `+`
918    /// lowers to tatara's numeric `+`; making it polymorphic would let a type
919    /// error at a seam disappear into a string.
920    #[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        // Ruby's `split("")` yields characters.
933        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    /// Reversed by character, so a multi-byte character survives. A byte-wise
986    /// reverse produces invalid UTF-8.
987    #[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    /// **`to_int` answers nil on garbage, not 0.** Ruby's `String#to_i` returns
999    /// 0, which silently turns a parse failure into a plausible number — a
1000    /// deliberate divergence, and the reason `to_int!` exists for callers who
1001    /// want the failure loudly.
1002    #[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    /// A wrong-typed argument is a typed error, not a silent coercion.
1031    #[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}