lox_lang/lox_std/
mod.rs

1use std::{
2    convert::TryInto, env, error::Error, ffi::OsString, fmt, fs, process::Command, time::Instant,
3};
4
5use crate::{Class, Instance, NativeFun, Value, VM};
6
7type StaticNativeFun = fn(&[Value]) -> Result<Value, Box<dyn Error>>;
8
9impl VM {
10    pub fn add_std_globals(&mut self) {
11        // free functions
12        self.define_closure_value("clock", create_clock_fn());
13        self.define_fn_value("readFile", read_file);
14        self.define_fn_value("writeFile", write_file);
15        self.define_fn_value("shell", shell);
16        self.define_fn_value("env", environment_var);
17        self.define_fn_value("setEnv", set_environment_var);
18
19        // global instances
20        self.add_arguments_instance();
21    }
22
23    fn define_fn_value(&mut self, name: &'static str, f: StaticNativeFun) {
24        let fn_value = Value::NativeFun(self.alloc(Box::new(f)));
25        self.define_global(name, fn_value);
26    }
27
28    fn define_closure_value(&mut self, name: &'static str, c: NativeFun) {
29        let fn_value = Value::NativeFun(self.alloc(c));
30        self.define_global(name, fn_value);
31    }
32
33    pub fn add_arguments_instance(&mut self) {
34        let args = env::args_os().collect::<Vec<_>>();
35        let count = args.len();
36
37        let get_arg = Box::new(move |a: &[Value]| match a {
38            [Value::Number(n)] if n.is_finite() && n.is_sign_positive() => Ok(args
39                .get(n.round().abs() as usize)
40                .map_or(Value::Nil, |val: &OsString| {
41                    val.to_string_lossy().as_ref().into()
42                })),
43            [Value::Number(_)] => Err(Box::new(LoxStdErr::InvalidArgument {
44                expected: "a positive, finite number as argument index",
45            }) as Box<dyn Error>),
46            _ if a.len() != 1 => Err(Box::new(LoxStdErr::ArityMismatch {
47                expected: 1,
48                got: a.len().try_into().unwrap_or(255),
49            }) as Box<dyn Error>),
50            _ => Err(Box::new(LoxStdErr::ArgumentTypes {
51                expected: "a number (command line argument index)",
52            }) as Box<dyn Error>),
53        });
54
55        let mut arg_class = Class::new(&"Arguments");
56        arg_class
57            .methods
58            .insert("get".into(), Value::NativeFun(self.alloc(get_arg)));
59
60        let mut arg_inst = Instance::new(self.alloc(arg_class));
61        arg_inst
62            .fields
63            .insert("count".into(), Value::Number(count as f64));
64        let arg_value = Value::Instance(self.alloc(arg_inst));
65        self.define_global("arguments", arg_value);
66    }
67}
68
69#[derive(Debug)]
70enum LoxStdErr {
71    ArityMismatch { expected: u8, got: u8 },
72    ArgumentTypes { expected: &'static str },
73    InvalidArgument { expected: &'static str },
74}
75
76impl fmt::Display for LoxStdErr {
77    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78        match self {
79            Self::ArityMismatch { expected, got } => {
80                write!(f, "expected {} arguments, got {}", expected, got)
81            }
82            Self::ArgumentTypes { expected } => {
83                write!(f, "wrong argument type(s) supplied: expected {}", expected)
84            }
85            Self::InvalidArgument { expected } => write!(
86                f,
87                "invalid argument value(s) supplied: expected {}",
88                expected
89            ),
90        }
91    }
92}
93
94impl Error for LoxStdErr {}
95
96/// Create a stateful `clock` function.
97///
98/// Comparable to the `clock` function in `time.h`. When invoked inside the runtime, it returns a
99/// floating-point number representing the number of seconds elapsed since the closure was created.
100#[must_use]
101pub fn create_clock_fn() -> NativeFun {
102    let start_time = Instant::now();
103    let clock_cls = move |_: &[Value]| Ok(start_time.elapsed().as_secs_f64().into());
104    Box::new(clock_cls) as NativeFun
105}
106
107/// Read a file into a string.
108///
109/// ### Errors
110///
111/// An `Err` will be returned if:
112///
113/// - The specified file is not found.
114/// - The specified file is not valid UTF-8.
115pub fn read_file(args: &[Value]) -> Result<Value, Box<dyn Error>> {
116    match args {
117        [Value::r#String(s)] => Ok(fs::read_to_string(s.as_ref())?.into()),
118        _ if args.len() != 1 => Err(Box::new(LoxStdErr::ArityMismatch {
119            expected: 1,
120            got: args.len().try_into().unwrap_or(255),
121        })),
122        _ => Err(Box::new(LoxStdErr::ArgumentTypes {
123            expected: "string for filename",
124        })),
125    }
126}
127
128/// Write a string into a file.
129///
130/// If the file does not already exist, it will be created. If it does exist, its contents will be
131/// replaced with the string passed in.
132///
133/// ### Errors
134///
135/// An `Err` will be returned if:
136///
137/// - The specified file is not found.
138/// - The specified file is not valid UTF-8.
139pub fn write_file(args: &[Value]) -> Result<Value, Box<dyn Error>> {
140    match args {
141        [Value::r#String(file_name), Value::r#String(contents)] => {
142            fs::write(file_name.as_ref(), contents.as_ref())?;
143            Ok(Value::Nil)
144        }
145        _ if args.len() != 2 => Err(Box::new(LoxStdErr::ArityMismatch {
146            expected: 2,
147            got: args.len().try_into().unwrap_or(255),
148        })),
149        _ => Err(Box::new(LoxStdErr::ArgumentTypes {
150            expected: "string for filename",
151        })),
152    }
153}
154
155/// Run a shell command.
156///
157/// ### Errors
158///
159/// An `Err` will be returned if the command fails to execute.
160pub fn shell(args: &[Value]) -> Result<Value, Box<dyn Error>> {
161    match args {
162        [Value::r#String(cmd)] => {
163            let output = if cfg!(target_os = "windows") {
164                Command::new("cmd").args(&["/C", &cmd]).output()?
165            } else {
166                Command::new("sh").args(&["-c", &cmd]).output()?
167            };
168
169            Ok(String::from_utf8_lossy(&output.stdout).as_ref().into())
170        }
171        _ if args.len() != 1 => Err(Box::new(LoxStdErr::ArityMismatch {
172            expected: 1,
173            got: args.len().try_into().unwrap_or(255),
174        })),
175        _ => Err(Box::new(LoxStdErr::ArgumentTypes {
176            expected: "string for shell command",
177        })),
178    }
179}
180
181/// Get the value of an environment variable by name.
182///
183/// If the requested variable does not exist, `nil` is returned. If the variable's value contains
184/// invalid Unicode, those characters will be replaced with the Unicode replacement character
185/// (`U+FFFD`).
186pub fn environment_var(args: &[Value]) -> Result<Value, Box<dyn Error>> {
187    match args {
188        [Value::r#String(name)] => {
189            Ok(env::var_os(name.as_ref())
190                .map_or(Value::Nil, |s| s.to_string_lossy().as_ref().into()))
191        }
192        _ if args.len() != 1 => Err(Box::new(LoxStdErr::ArityMismatch {
193            expected: 1,
194            got: args.len().try_into().unwrap_or(255),
195        })),
196        _ => Err(Box::new(LoxStdErr::ArgumentTypes {
197            expected: "name of environment variable",
198        })),
199    }
200}
201
202/// Set the value of an environment variable.
203pub fn set_environment_var(args: &[Value]) -> Result<Value, Box<dyn Error>> {
204    match args {
205        [Value::r#String(name), Value::r#String(value)] => {
206            env::set_var(name.as_ref(), value.as_ref());
207            Ok(Value::Nil)
208        }
209        _ if args.len() != 2 => Err(Box::new(LoxStdErr::ArityMismatch {
210            expected: 2,
211            got: args.len().try_into().unwrap_or(255),
212        })),
213        _ => Err(Box::new(LoxStdErr::ArgumentTypes {
214            expected: "name of environment variable",
215        })),
216    }
217}