1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//! String helpers beyond what tatara-lisp-eval::primitive provides.
//!
//! (string-replace STR FROM TO) → new string
//! (string-split STR SEP) → list of strings
//! (string-contains? STR NEEDLE) → bool
//! (string-lowercase STR) → new string
//! (string-format TMPL &rest VS) → printf-ish, `{}` placeholders consumed left-to-right
//! (string-trim STR) → new string with leading/trailing whitespace removed
//! (string->number STR) → Int when the text is an integer, else Float;
//! a typed error when it is neither
use std::sync::Arc;
use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
use crate::script_ctx::ScriptCtx;
use crate::stdlib::env::str_arg;
pub fn install(interp: &mut Interpreter<ScriptCtx>) {
interp.register_fn(
"string-replace",
Arity::Exact(3),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let s = str_arg(&args[0], "string-replace", sp)?;
let from = str_arg(&args[1], "string-replace", sp)?;
let to = str_arg(&args[2], "string-replace", sp)?;
Ok(Value::Str(Arc::from(s.replace(&*from, &to))))
},
);
interp.register_fn(
"string-split",
Arity::Exact(2),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let s = str_arg(&args[0], "string-split", sp)?;
let sep = str_arg(&args[1], "string-split", sp)?;
Ok(Value::list(
s.split(&*sep)
.map(|p| Value::Str(Arc::from(p)))
.collect::<Vec<_>>(),
))
},
);
interp.register_fn(
"string-contains?",
Arity::Exact(2),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let s = str_arg(&args[0], "string-contains?", sp)?;
let needle = str_arg(&args[1], "string-contains?", sp)?;
Ok(Value::Bool(s.contains(&*needle)))
},
);
interp.register_fn(
"string-lowercase",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let s = str_arg(&args[0], "string-lowercase", sp)?;
Ok(Value::Str(Arc::from(s.to_lowercase())))
},
);
interp.register_fn(
"string-trim",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let s = str_arg(&args[0], "string-trim", sp)?;
Ok(Value::Str(Arc::from(s.trim())))
},
);
// (string->number STR)
//
// The gap this closes: every value that arrives from outside a script —
// a captured stdout, a matched group, a JSON string field — is text, and
// without this there is no way to do arithmetic on it. Scripts were
// working around it by folding over `string-chars`, which handles neither
// a sign nor a decimal point, or by pushing the arithmetic out into a
// shell one-liner, which is the thing the stack law exists to prevent.
//
// Integer-first, deliberately: `"60"` must come back as Int so it can index,
// compare and add against other Ints without a silent float contaminating
// the result. Only text that is not a valid integer is tried as a float.
// Failure is a typed error, never a silent 0 — a 0 returned for "abc" is
// the same shape of defect as an unreachable cluster reported as an empty
// one.
interp.register_fn(
"string->number",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let s = str_arg(&args[0], "string->number", sp)?;
let t = s.trim();
if let Ok(n) = t.parse::<i64>() {
return Ok(Value::Int(n));
}
if let Ok(f) = t.parse::<f64>() {
if f.is_finite() {
return Ok(Value::Float(f));
}
}
Err(EvalError::native_fn(
"string->number",
format!("not a number: {t:?}"),
sp,
))
},
);
interp.register_fn(
"string-format",
Arity::AtLeast(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let tmpl = str_arg(&args[0], "string-format", sp)?;
let mut out = String::with_capacity(tmpl.len());
let mut remaining = &*tmpl;
let mut idx = 1usize;
while let Some(pos) = remaining.find("{}") {
out.push_str(&remaining[..pos]);
let Some(arg) = args.get(idx) else {
return Err(EvalError::native_fn(
"string-format",
format!("template has more {{}} than args ({idx})"),
sp,
));
};
out.push_str(&display_value(arg));
remaining = &remaining[pos + 2..];
idx += 1;
}
out.push_str(remaining);
Ok(Value::Str(Arc::from(out)))
},
);
}
fn display_value(v: &Value) -> String {
match v {
Value::Nil => "nil".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::Float(n) => n.to_string(),
Value::Str(s) | Value::Symbol(s) | Value::Keyword(s) => s.as_ref().to_string(),
other => format!("{other:?}"),
}
}