use std::{
convert::TryInto, env, error::Error, ffi::OsString, fmt, fs, process::Command, time::Instant,
};
use crate::{Class, Instance, NativeFun, Value, VM};
type StaticNativeFun = fn(&[Value]) -> Result<Value, Box<dyn Error>>;
impl VM {
pub fn add_std_globals(&mut self) {
self.define_closure_value("clock", create_clock_fn());
self.define_fn_value("readFile", read_file);
self.define_fn_value("writeFile", write_file);
self.define_fn_value("shell", shell);
self.define_fn_value("env", environment_var);
self.define_fn_value("setEnv", set_environment_var);
self.add_arguments_instance();
}
fn define_fn_value(&mut self, name: &'static str, f: StaticNativeFun) {
let fn_value = Value::NativeFun(self.alloc(Box::new(f)));
self.define_global(name, fn_value);
}
fn define_closure_value(&mut self, name: &'static str, c: NativeFun) {
let fn_value = Value::NativeFun(self.alloc(c));
self.define_global(name, fn_value);
}
pub fn add_arguments_instance(&mut self) {
let args = env::args_os().collect::<Vec<_>>();
let count = args.len();
let get_arg = Box::new(move |a: &[Value]| match a {
[Value::Number(n)] if n.is_finite() && n.is_sign_positive() => Ok(args
.get(n.round().abs() as usize)
.map_or(Value::Nil, |val: &OsString| {
val.to_string_lossy().as_ref().into()
})),
[Value::Number(_)] => Err(Box::new(LoxStdErr::InvalidArgument {
expected: "a positive, finite number as argument index",
}) as Box<dyn Error>),
_ if a.len() != 1 => Err(Box::new(LoxStdErr::ArityMismatch {
expected: 1,
got: a.len().try_into().unwrap_or(255),
}) as Box<dyn Error>),
_ => Err(Box::new(LoxStdErr::ArgumentTypes {
expected: "a number (command line argument index)",
}) as Box<dyn Error>),
});
let mut arg_class = Class::new(&"Arguments");
arg_class
.methods
.insert("get".into(), Value::NativeFun(self.alloc(get_arg)));
let mut arg_inst = Instance::new(self.alloc(arg_class));
arg_inst
.fields
.insert("count".into(), Value::Number(count as f64));
let arg_value = Value::Instance(self.alloc(arg_inst));
self.define_global("arguments", arg_value);
}
}
#[derive(Debug)]
enum LoxStdErr {
ArityMismatch { expected: u8, got: u8 },
ArgumentTypes { expected: &'static str },
InvalidArgument { expected: &'static str },
}
impl fmt::Display for LoxStdErr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::ArityMismatch { expected, got } => {
write!(f, "expected {} arguments, got {}", expected, got)
}
Self::ArgumentTypes { expected } => {
write!(f, "wrong argument type(s) supplied: expected {}", expected)
}
Self::InvalidArgument { expected } => write!(
f,
"invalid argument value(s) supplied: expected {}",
expected
),
}
}
}
impl Error for LoxStdErr {}
#[must_use]
pub fn create_clock_fn() -> NativeFun {
let start_time = Instant::now();
let clock_cls = move |_: &[Value]| Ok(start_time.elapsed().as_secs_f64().into());
Box::new(clock_cls) as NativeFun
}
pub fn read_file(args: &[Value]) -> Result<Value, Box<dyn Error>> {
match args {
[Value::r#String(s)] => Ok(fs::read_to_string(s.as_ref())?.into()),
_ if args.len() != 1 => Err(Box::new(LoxStdErr::ArityMismatch {
expected: 1,
got: args.len().try_into().unwrap_or(255),
})),
_ => Err(Box::new(LoxStdErr::ArgumentTypes {
expected: "string for filename",
})),
}
}
pub fn write_file(args: &[Value]) -> Result<Value, Box<dyn Error>> {
match args {
[Value::r#String(file_name), Value::r#String(contents)] => {
fs::write(file_name.as_ref(), contents.as_ref())?;
Ok(Value::Nil)
}
_ if args.len() != 2 => Err(Box::new(LoxStdErr::ArityMismatch {
expected: 2,
got: args.len().try_into().unwrap_or(255),
})),
_ => Err(Box::new(LoxStdErr::ArgumentTypes {
expected: "string for filename",
})),
}
}
pub fn shell(args: &[Value]) -> Result<Value, Box<dyn Error>> {
match args {
[Value::r#String(cmd)] => {
let output = if cfg!(target_os = "windows") {
Command::new("cmd").args(&["/C", &cmd]).output()?
} else {
Command::new("sh").args(&["-c", &cmd]).output()?
};
Ok(String::from_utf8_lossy(&output.stdout).as_ref().into())
}
_ if args.len() != 1 => Err(Box::new(LoxStdErr::ArityMismatch {
expected: 1,
got: args.len().try_into().unwrap_or(255),
})),
_ => Err(Box::new(LoxStdErr::ArgumentTypes {
expected: "string for shell command",
})),
}
}
pub fn environment_var(args: &[Value]) -> Result<Value, Box<dyn Error>> {
match args {
[Value::r#String(name)] => {
Ok(env::var_os(name.as_ref())
.map_or(Value::Nil, |s| s.to_string_lossy().as_ref().into()))
}
_ if args.len() != 1 => Err(Box::new(LoxStdErr::ArityMismatch {
expected: 1,
got: args.len().try_into().unwrap_or(255),
})),
_ => Err(Box::new(LoxStdErr::ArgumentTypes {
expected: "name of environment variable",
})),
}
}
pub fn set_environment_var(args: &[Value]) -> Result<Value, Box<dyn Error>> {
match args {
[Value::r#String(name), Value::r#String(value)] => {
env::set_var(name.as_ref(), value.as_ref());
Ok(Value::Nil)
}
_ if args.len() != 2 => Err(Box::new(LoxStdErr::ArityMismatch {
expected: 2,
got: args.len().try_into().unwrap_or(255),
})),
_ => Err(Box::new(LoxStdErr::ArgumentTypes {
expected: "name of environment variable",
})),
}
}