1use super::corelib::string_key;
2use crate::bytecode::{NativeCallResult, Value, ValueKey};
3use crate::config::LustConfig;
4use crate::LustInt;
5use hashbrown::HashMap;
6use std::fs;
7use std::io::{self, Read, Write};
8use std::rc::Rc;
9use std::thread;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11pub fn create_stdlib(config: &LustConfig) -> Vec<(&'static str, Value)> {
12 let mut stdlib = vec![
13 ("print", create_print_fn()),
14 ("println", create_println_fn()),
15 ("type", create_type_fn()),
16 ];
17 if config.is_module_enabled("io") {
18 stdlib.push(("io", create_io_module()));
19 }
20
21 if config.is_module_enabled("os") {
22 stdlib.push(("os", create_os_module()));
23 }
24
25 stdlib
26}
27
28fn create_print_fn() -> Value {
29 Value::NativeFunction(Rc::new(|args: &[Value]| {
30 for (i, arg) in args.iter().enumerate() {
31 if i > 0 {
32 print!("\t");
33 }
34
35 print!("{}", arg);
36 }
37
38 Ok(NativeCallResult::Return(Value::Nil))
39 }))
40}
41
42fn create_println_fn() -> Value {
43 Value::NativeFunction(Rc::new(|args: &[Value]| {
44 for (i, arg) in args.iter().enumerate() {
45 if i > 0 {
46 print!("\t");
47 }
48
49 print!("{}", arg);
50 }
51
52 println!();
53 Ok(NativeCallResult::Return(Value::Nil))
54 }))
55}
56
57fn create_type_fn() -> Value {
58 Value::NativeFunction(Rc::new(|args: &[Value]| {
59 if args.is_empty() {
60 return Err("type() requires at least one argument".to_string());
61 }
62
63 let type_name = match &args[0] {
64 Value::Nil => "nil",
65 Value::Bool(_) => "bool",
66 Value::Int(_) => "int",
67 Value::Float(_) => "float",
68 Value::String(_) => "string",
69 Value::Array(_) => "array",
70 Value::Tuple(_) => "tuple",
71 Value::Map(_) => "map",
72 Value::Struct { .. } | Value::WeakStruct(_) => "struct",
73 Value::Enum { .. } => "enum",
74 Value::Function(_) => "function",
75 Value::NativeFunction(_) => "function",
76 Value::Closure { .. } => "function",
77 Value::Iterator(_) => "iterator",
78 Value::Task(_) => "task",
79 };
80 Ok(NativeCallResult::Return(Value::string(type_name)))
81 }))
82}
83
84fn create_io_module() -> Value {
85 let mut entries: HashMap<ValueKey, Value> = HashMap::new();
86 entries.insert(string_key("read_file"), create_io_read_file_fn());
87 entries.insert(
88 string_key("read_file_bytes"),
89 create_io_read_file_bytes_fn(),
90 );
91 entries.insert(string_key("write_file"), create_io_write_file_fn());
92 entries.insert(string_key("read_stdin"), create_io_read_stdin_fn());
93 entries.insert(string_key("read_line"), create_io_read_line_fn());
94 entries.insert(string_key("write_stdout"), create_io_write_stdout_fn());
95 Value::map(entries)
96}
97
98fn create_io_read_file_fn() -> Value {
99 Value::NativeFunction(Rc::new(|args: &[Value]| {
100 if args.len() != 1 {
101 return Ok(NativeCallResult::Return(Value::err(Value::string(
102 "io.read_file(path) requires a single string path",
103 ))));
104 }
105
106 let path = match args[0].as_string() {
107 Some(p) => p,
108 None => {
109 return Ok(NativeCallResult::Return(Value::err(Value::string(
110 "io.read_file(path) requires a string path",
111 ))))
112 }
113 };
114 match fs::read_to_string(path) {
115 Ok(contents) => Ok(NativeCallResult::Return(Value::ok(Value::string(contents)))),
116 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
117 err.to_string(),
118 )))),
119 }
120 }))
121}
122
123fn create_io_read_file_bytes_fn() -> Value {
124 Value::NativeFunction(Rc::new(|args: &[Value]| {
125 if args.len() != 1 {
126 return Ok(NativeCallResult::Return(Value::err(Value::string(
127 "io.read_file_bytes(path) requires a single string path",
128 ))));
129 }
130
131 let path = match args[0].as_string() {
132 Some(p) => p,
133 None => {
134 return Ok(NativeCallResult::Return(Value::err(Value::string(
135 "io.read_file_bytes(path) requires a string path",
136 ))))
137 }
138 };
139
140 match fs::read(path) {
141 Ok(bytes) => {
142 let values: Vec<Value> = bytes
143 .into_iter()
144 .map(|b| Value::Int(b as LustInt))
145 .collect();
146 Ok(NativeCallResult::Return(Value::ok(Value::array(values))))
147 }
148
149 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
150 err.to_string(),
151 )))),
152 }
153 }))
154}
155
156fn create_io_write_file_fn() -> Value {
157 Value::NativeFunction(Rc::new(|args: &[Value]| {
158 if args.len() < 2 {
159 return Ok(NativeCallResult::Return(Value::err(Value::string(
160 "io.write_file(path, contents) requires a path and value",
161 ))));
162 }
163
164 let path = match args[0].as_string() {
165 Some(p) => p,
166 None => {
167 return Ok(NativeCallResult::Return(Value::err(Value::string(
168 "io.write_file(path, contents) requires a string path",
169 ))))
170 }
171 };
172 let contents = if let Some(s) = args[1].as_string() {
173 s.to_string()
174 } else {
175 format!("{}", args[1])
176 };
177 match fs::write(path, contents) {
178 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
179 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
180 err.to_string(),
181 )))),
182 }
183 }))
184}
185
186fn create_io_read_stdin_fn() -> Value {
187 Value::NativeFunction(Rc::new(|args: &[Value]| {
188 if !args.is_empty() {
189 return Ok(NativeCallResult::Return(Value::err(Value::string(
190 "io.read_stdin() takes no arguments",
191 ))));
192 }
193
194 let mut buffer = String::new();
195 match io::stdin().read_to_string(&mut buffer) {
196 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::string(buffer)))),
197 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
198 err.to_string(),
199 )))),
200 }
201 }))
202}
203
204fn create_io_read_line_fn() -> Value {
205 Value::NativeFunction(Rc::new(|args: &[Value]| {
206 if !args.is_empty() {
207 return Ok(NativeCallResult::Return(Value::err(Value::string(
208 "io.read_line() takes no arguments",
209 ))));
210 }
211
212 let mut line = String::new();
213 match io::stdin().read_line(&mut line) {
214 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::string(line)))),
215 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
216 err.to_string(),
217 )))),
218 }
219 }))
220}
221
222fn create_io_write_stdout_fn() -> Value {
223 Value::NativeFunction(Rc::new(|args: &[Value]| {
224 let mut stdout = io::stdout();
225 for arg in args {
226 if let Err(err) = write!(stdout, "{}", arg) {
227 return Ok(NativeCallResult::Return(Value::err(Value::string(
228 err.to_string(),
229 ))));
230 }
231 }
232
233 if let Err(err) = stdout.flush() {
234 return Ok(NativeCallResult::Return(Value::err(Value::string(
235 err.to_string(),
236 ))));
237 }
238
239 Ok(NativeCallResult::Return(Value::ok(Value::Nil)))
240 }))
241}
242
243fn create_os_module() -> Value {
244 let mut entries: HashMap<ValueKey, Value> = HashMap::new();
245 entries.insert(string_key("time"), create_os_time_fn());
246 entries.insert(string_key("sleep"), create_os_sleep_fn());
247 entries.insert(string_key("create_file"), create_os_create_file_fn());
248 entries.insert(string_key("create_dir"), create_os_create_dir_fn());
249 entries.insert(string_key("remove_file"), create_os_remove_file_fn());
250 entries.insert(string_key("remove_dir"), create_os_remove_dir_fn());
251 entries.insert(string_key("rename"), create_os_rename_fn());
252 Value::map(entries)
253}
254
255fn create_os_time_fn() -> Value {
256 Value::NativeFunction(Rc::new(|args: &[Value]| {
257 if !args.is_empty() {
258 return Ok(NativeCallResult::Return(Value::err(Value::string(
259 "os.time() takes no arguments",
260 ))));
261 }
262
263 let now = SystemTime::now();
264 let seconds = match now.duration_since(UNIX_EPOCH) {
265 Ok(duration) => duration.as_secs_f64(),
266 Err(err) => -(err.duration().as_secs_f64()),
267 };
268
269 Ok(NativeCallResult::Return(Value::Float(seconds)))
270 }))
271}
272
273fn create_os_sleep_fn() -> Value {
274 Value::NativeFunction(Rc::new(|args: &[Value]| {
275 if args.len() != 1 {
276 return Ok(NativeCallResult::Return(Value::err(Value::string(
277 "os.sleep(seconds) requires a single float duration",
278 ))));
279 }
280
281 let seconds = match args[0].as_float() {
282 Some(value) => value,
283 None => {
284 return Ok(NativeCallResult::Return(Value::err(Value::string(
285 "os.sleep(seconds) requires a float duration",
286 ))))
287 }
288 };
289
290 if !seconds.is_finite() || seconds < 0.0 {
291 return Ok(NativeCallResult::Return(Value::err(Value::string(
292 "os.sleep(seconds) requires a finite, non-negative duration",
293 ))));
294 }
295
296 if seconds > (u64::MAX as f64) {
297 return Ok(NativeCallResult::Return(Value::err(Value::string(
298 "os.sleep(seconds) duration is too large",
299 ))));
300 }
301
302 thread::sleep(Duration::from_secs_f64(seconds));
303
304 Ok(NativeCallResult::Return(Value::ok(Value::Nil)))
305 }))
306}
307
308fn create_os_create_file_fn() -> Value {
309 Value::NativeFunction(Rc::new(|args: &[Value]| {
310 if args.len() != 1 {
311 return Ok(NativeCallResult::Return(Value::err(Value::string(
312 "os.create_file(path) requires a single string path",
313 ))));
314 }
315
316 let path = match args[0].as_string() {
317 Some(p) => p,
318 None => {
319 return Ok(NativeCallResult::Return(Value::err(Value::string(
320 "os.create_file(path) requires a string path",
321 ))))
322 }
323 };
324 match fs::OpenOptions::new().write(true).create(true).open(path) {
325 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
326 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
327 err.to_string(),
328 )))),
329 }
330 }))
331}
332
333fn create_os_create_dir_fn() -> Value {
334 Value::NativeFunction(Rc::new(|args: &[Value]| {
335 if args.len() != 1 {
336 return Ok(NativeCallResult::Return(Value::err(Value::string(
337 "os.create_dir(path) requires a single string path",
338 ))));
339 }
340
341 let path = match args[0].as_string() {
342 Some(p) => p,
343 None => {
344 return Ok(NativeCallResult::Return(Value::err(Value::string(
345 "os.create_dir(path) requires a string path",
346 ))))
347 }
348 };
349 match fs::create_dir_all(path) {
350 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
351 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
352 err.to_string(),
353 )))),
354 }
355 }))
356}
357
358fn create_os_remove_file_fn() -> Value {
359 Value::NativeFunction(Rc::new(|args: &[Value]| {
360 if args.len() != 1 {
361 return Ok(NativeCallResult::Return(Value::err(Value::string(
362 "os.remove_file(path) requires a single string path",
363 ))));
364 }
365
366 let path = match args[0].as_string() {
367 Some(p) => p,
368 None => {
369 return Ok(NativeCallResult::Return(Value::err(Value::string(
370 "os.remove_file(path) requires a string path",
371 ))))
372 }
373 };
374 match fs::remove_file(path) {
375 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
376 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
377 err.to_string(),
378 )))),
379 }
380 }))
381}
382
383fn create_os_remove_dir_fn() -> Value {
384 Value::NativeFunction(Rc::new(|args: &[Value]| {
385 if args.len() != 1 {
386 return Ok(NativeCallResult::Return(Value::err(Value::string(
387 "os.remove_dir(path) requires a single string path",
388 ))));
389 }
390
391 let path = match args[0].as_string() {
392 Some(p) => p,
393 None => {
394 return Ok(NativeCallResult::Return(Value::err(Value::string(
395 "os.remove_dir(path) requires a string path",
396 ))))
397 }
398 };
399 match fs::remove_dir_all(path) {
400 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
401 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
402 err.to_string(),
403 )))),
404 }
405 }))
406}
407
408fn create_os_rename_fn() -> Value {
409 Value::NativeFunction(Rc::new(|args: &[Value]| {
410 if args.len() != 2 {
411 return Ok(NativeCallResult::Return(Value::err(Value::string(
412 "os.rename(from, to) requires two string paths",
413 ))));
414 }
415
416 let from = match args[0].as_string() {
417 Some(f) => f,
418 None => {
419 return Ok(NativeCallResult::Return(Value::err(Value::string(
420 "os.rename(from, to) requires string paths",
421 ))))
422 }
423 };
424 let to = match args[1].as_string() {
425 Some(t) => t,
426 None => {
427 return Ok(NativeCallResult::Return(Value::err(Value::string(
428 "os.rename(from, to) requires string paths",
429 ))))
430 }
431 };
432 match fs::rename(from, to) {
433 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
434 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
435 err.to_string(),
436 )))),
437 }
438 }))
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 #[test]
445 fn stdlib_defaults_without_optional_modules() {
446 let stdlib = create_stdlib(&LustConfig::default());
447 assert!(!stdlib.iter().any(|(name, _)| *name == "io"));
448 assert!(!stdlib.iter().any(|(name, _)| *name == "os"));
449 }
450
451 #[test]
452 fn stdlib_includes_optional_modules_when_configured() {
453 let cfg = LustConfig::from_toml_str(
454 r#"
455 [settings]
456 stdlib_modules = ["io", "os"]
457 "#,
458 )
459 .expect("parse");
460 let stdlib = create_stdlib(&cfg);
461 assert!(stdlib.iter().any(|(name, _)| *name == "io"));
462 assert!(stdlib.iter().any(|(name, _)| *name == "os"));
463 }
464}