lust/vm/
stdlib.rs

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}