1use std::collections::BTreeMap;
26use std::sync::Arc;
27
28use crate::error::{EvalError, Result};
29use crate::value::{Arity, Builtin, BuiltinFn, Value};
30
31pub fn system_builtin_table() -> BTreeMap<String, Value> {
32 let mut m: BTreeMap<String, Value> = BTreeMap::new();
33 for b in all_system_builtins() {
34 let name = b.name.clone();
35 m.insert(name, Value::Builtin(Arc::new(b)));
36 }
37 m
38}
39
40fn all_system_builtins() -> Vec<Builtin> {
41 vec![
42 mk("print", Arity::Exact(1), Arc::new(print_)),
43 mk("println", Arity::Exact(1), Arc::new(println_)),
44 mk("eprint", Arity::Exact(1), Arc::new(eprint_)),
45 mk("eprintln", Arity::Exact(1), Arc::new(eprintln_)),
46 mk("sleep", Arity::Exact(1), Arc::new(sleep_)),
47 mk("exit", Arity::Exact(1), Arc::new(exit_)),
48 mk("env", Arity::Exact(1), Arc::new(env_)),
49 mk("env-or", Arity::Exact(2), Arc::new(env_or_)),
50 mk("read-file", Arity::Exact(1), Arc::new(read_file_)),
51 mk("write-file", Arity::Exact(2), Arc::new(write_file_)),
52 mk("shell", Arity::Exact(1), Arc::new(shell_)),
53 ]
54}
55
56fn mk(name: &str, arity: Arity, f: Arc<BuiltinFn>) -> Builtin {
57 Builtin {
58 name: name.into(),
59 arity,
60 func: f,
61 }
62}
63
64fn to_display_string(v: &Value) -> String {
65 v.coerce_to_string().unwrap_or_else(|| format!("{v:?}"))
66}
67
68fn print_(args: &[Value]) -> Result<Value> {
69 print!("{}", to_display_string(&args[0]));
70 use std::io::Write;
71 let _ = std::io::stdout().flush();
72 Ok(Value::Nil)
73}
74
75fn println_(args: &[Value]) -> Result<Value> {
76 println!("{}", to_display_string(&args[0]));
77 Ok(Value::Nil)
78}
79
80fn eprint_(args: &[Value]) -> Result<Value> {
81 eprint!("{}", to_display_string(&args[0]));
82 use std::io::Write;
83 let _ = std::io::stderr().flush();
84 Ok(Value::Nil)
85}
86
87fn eprintln_(args: &[Value]) -> Result<Value> {
88 eprintln!("{}", to_display_string(&args[0]));
89 Ok(Value::Nil)
90}
91
92fn sleep_(args: &[Value]) -> Result<Value> {
93 let seconds = match &args[0] {
94 Value::Int(n) if *n >= 0 => *n as f64,
95 Value::Float(f) if *f >= 0.0 => *f,
96 v => {
97 return Err(EvalError::Type {
98 expected: "non-negative int or float (seconds)".into(),
99 found: v.type_name().into(),
100 })
101 }
102 };
103 let duration = std::time::Duration::from_secs_f64(seconds);
104 std::thread::sleep(duration);
105 Ok(Value::Nil)
106}
107
108fn exit_(args: &[Value]) -> Result<Value> {
109 let code = args[0].as_int().unwrap_or(0) as i32;
110 std::process::exit(code);
111}
112
113fn env_(args: &[Value]) -> Result<Value> {
114 let name = args[0].as_str().ok_or_else(|| EvalError::Type {
115 expected: "string".into(),
116 found: args[0].type_name().into(),
117 })?;
118 match std::env::var(name) {
119 Ok(v) => Ok(Value::Str(v)),
120 Err(_) => Ok(Value::Nil),
121 }
122}
123
124fn env_or_(args: &[Value]) -> Result<Value> {
125 let name = args[0].as_str().ok_or_else(|| EvalError::Type {
126 expected: "string (env var name)".into(),
127 found: args[0].type_name().into(),
128 })?;
129 let default = args[1].clone();
130 match std::env::var(name) {
131 Ok(v) => Ok(Value::Str(v)),
132 Err(_) => Ok(default),
133 }
134}
135
136fn read_file_(args: &[Value]) -> Result<Value> {
137 let path = args[0]
138 .as_path()
139 .map(|p| p.clone())
140 .or_else(|| args[0].as_str().map(std::path::PathBuf::from))
141 .ok_or_else(|| EvalError::Type {
142 expected: "string or path".into(),
143 found: args[0].type_name().into(),
144 })?;
145 let s = std::fs::read_to_string(&path)?;
146 Ok(Value::Str(s))
147}
148
149fn write_file_(args: &[Value]) -> Result<Value> {
150 let path = args[0]
151 .as_path()
152 .map(|p| p.clone())
153 .or_else(|| args[0].as_str().map(std::path::PathBuf::from))
154 .ok_or_else(|| EvalError::Type {
155 expected: "string or path (write-file first arg)".into(),
156 found: args[0].type_name().into(),
157 })?;
158 let content = args[1].coerce_to_string().ok_or_else(|| EvalError::Type {
159 expected: "string-coercible".into(),
160 found: args[1].type_name().into(),
161 })?;
162 if let Some(parent) = path.parent() {
163 std::fs::create_dir_all(parent)?;
164 }
165 std::fs::write(&path, content)?;
166 Ok(Value::Nil)
167}
168
169fn shell_(args: &[Value]) -> Result<Value> {
170 let cmd = args[0].as_str().ok_or_else(|| EvalError::Type {
171 expected: "string".into(),
172 found: args[0].type_name().into(),
173 })?;
174 let status = std::process::Command::new("/bin/sh")
175 .arg("-c")
176 .arg(cmd)
177 .status()?;
178 Ok(Value::Int(status.code().unwrap_or(-1) as i64))
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn env_returns_nil_for_missing() {
187 let r = env_(&[Value::Str(
188 "TATARA_THIS_VAR_DEFINITELY_DOES_NOT_EXIST_12345".into(),
189 )])
190 .unwrap();
191 assert!(matches!(r, Value::Nil));
192 }
193
194 #[test]
195 fn env_or_returns_default_for_missing() {
196 let r = env_or_(&[
197 Value::Str("TATARA_MISSING_VAR_54321".into()),
198 Value::Str("fallback".into()),
199 ])
200 .unwrap();
201 assert!(matches!(r, Value::Str(s) if s == "fallback"));
202 }
203
204 #[test]
205 fn read_file_and_write_file_round_trip() {
206 let td = tempfile::tempdir().unwrap();
207 let path = td.path().join("hi.txt");
208 write_file_(&[
209 Value::Str(path.to_string_lossy().into_owned()),
210 Value::Str("hello lisp".into()),
211 ])
212 .unwrap();
213 let r = read_file_(&[Value::Str(path.to_string_lossy().into_owned())]).unwrap();
214 assert!(matches!(r, Value::Str(s) if s == "hello lisp"));
215 }
216
217 #[test]
218 fn shell_returns_exit_code() {
219 let r = shell_(&[Value::Str("true".into())]).unwrap();
220 assert!(matches!(r, Value::Int(0)));
221 let r = shell_(&[Value::Str("false".into())]).unwrap();
222 assert!(matches!(r, Value::Int(n) if n != 0));
223 }
224
225 #[test]
226 fn sleep_rejects_negative_seconds() {
227 let r = sleep_(&[Value::Int(-5)]);
228 assert!(r.is_err());
229 }
230}