treescript-interpreter 0.1.1

TreeScript interpreter, with some built-in libraries and API to create more
Documentation
extern crate app_dirs;
extern crate serde;
use crate::parse::Parser;
use crate::print::Printer;
use crate::util::{AtomicFile, AtomicFileCommit};
use crate::value::Value;
use app_dirs::{AppDataType, AppInfo};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::fs::{DirEntry, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};

#[derive(Clone, Debug, Deserialize, Serialize)]
struct LanguageSpec {
  name: String,
  extension: String,
}

pub struct Language {
  pub name: String,
  pub extension: String,
  dir: PathBuf,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LibrarySpec {
  code_name: String,
  dir_name: String,
}

struct Library {
  name: String,
  process: Option<Child>,
}

pub struct Session {
  langs: Vec<Language>,
  lang_idxs_by_name: HashMap<String, usize>,
  lang_idxs_by_ext: HashMap<String, usize>,
  libs: HashMap<String, Library>,
  root_dir: PathBuf,
}

impl Language {
  fn new(dir: DirEntry) -> Language {
    let dir_name = dir.file_name();
    let inferred_name = dir_name.to_string_lossy();
    let dir_path = dir.path();
    let mut spec_path = dir_path.clone();
    spec_path.push("spec.yaml");
    let spec_file = File::open(spec_path).expect(
      format!(
        "failed to read language specification for '{}'",
        inferred_name
      )
      .as_str(),
    );
    let spec: LanguageSpec = serde_yaml::from_reader(spec_file)
      .expect(format!("invalid language specification for '{}'", inferred_name).as_str());
    return Language {
      name: spec.name,
      extension: spec.extension,
      dir: dir_path,
    };
  }

  pub fn encode_ast<R: Into<Stdio>>(&self, in_code: R) -> ChildStdout {
    let mut parser_path = self.dir.clone();
    parser_path.push("parser");
    return Command::new(parser_path.as_os_str())
      .stdin(in_code)
      .stdout(Stdio::piped())
      .spawn()
      .expect(format!("failed to start parser for language: {}", self.name).as_str())
      .stdout
      .unwrap();
  }

  pub fn decode_ast<W: Into<Stdio>>(&self, out_code: W) -> ChildStdin {
    let mut printer_path = self.dir.clone();
    printer_path.push("printer");
    return Command::new(printer_path.as_os_str())
      .stdin(Stdio::piped())
      .stdout(out_code)
      .spawn()
      .expect(format!("failed to start printer for language: {}", self.name).as_str())
      .stdin
      .unwrap();
  }
}

impl Library {
  fn new(name: String) -> Library {
    return Library {
      name: name,
      process: Option::None,
    };
  }

  fn start(&mut self, path: &Option<PathBuf>, libs_dir: &PathBuf) {
    assert!(
      self.process.is_none(),
      "can't start session - already started"
    );
    let mut lib_path = libs_dir.clone();
    lib_path.push(self.name.clone());
    lib_path.push("exec");
    self.process = Option::Some(
      Command::new(lib_path.as_os_str())
        .args(path.into_iter())
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .expect(format!("failed to start library process: {}", self.name).as_str()),
    );
  }

  fn call_fun(&mut self, name: String, args: Vec<Value>) -> Value {
    let process = self
      .process
      .as_mut()
      .expect("can't call function - not started");
    let mut parser = Parser {
      input: process
        .stdout
        .as_mut()
        .expect("can't call function - process output not available"),
    };
    let mut printer = Printer {
      output: process
        .stdin
        .as_mut()
        .expect("can't call function - process input not available"),
    };
    printer
      .print_value(Value::Record {
        head: name,
        props: args,
      })
      .expect("can't call function - couldn't send input to library process");
    return parser
      .scan_value()
      .expect("can't call function - library process didn't give output");
  }

  fn stop(&mut self) {
    let mut process = self
      .process
      .take()
      .expect("can't stop session - not started");
    process
      .kill()
      .expect(format!("library process exited early: {}", self.name).as_str());
  }
}

impl Session {
  fn root_dir() -> PathBuf {
    return app_dirs::get_app_root(
      AppDataType::UserData,
      &AppInfo {
        name: "treescript",
        author: "jakobeha",
      },
    )
    .expect("failed to get user session directory");
  }

  pub fn new(libs: Vec<LibrarySpec>) -> Session {
    let root_dir = Session::root_dir();
    let mut langs_dir = root_dir.clone();
    langs_dir.push("languages");
    let lang_dirs = fs::read_dir(langs_dir)
      .expect("failed to find languages")
      .map(|lang_dir| lang_dir.expect("failed to find a language"))
      .filter(|lang_dir| lang_dir.file_name() != ".DS_Store");
    let langs: Vec<Language> = lang_dirs.map(|lang_dir| Language::new(lang_dir)).collect();
    let lang_idxs_by_name = langs
      .iter()
      .enumerate()
      .map(|(idx, lang)| (lang.name.clone(), idx))
      .collect();
    let lang_idxs_by_ext = langs
      .iter()
      .enumerate()
      .map(|(idx, lang)| (lang.extension.clone(), idx))
      .collect();
    return Session {
      langs: langs,
      lang_idxs_by_name: lang_idxs_by_name,
      lang_idxs_by_ext: lang_idxs_by_ext,
      libs: libs
        .into_iter()
        .map(|lib| (lib.code_name, Library::new(lib.dir_name)))
        .collect(),
      root_dir: root_dir,
    };
  }

  pub fn lang_with_name(&self, name: &String) -> Option<&Language> {
    return self
      .lang_idxs_by_name
      .get(name)
      .map(|idx| &self.langs[*idx]);
  }

  fn lang_with_ext(&self, ext: &String) -> Option<&Language> {
    return self.lang_idxs_by_ext.get(ext).map(|idx| &self.langs[*idx]);
  }

  pub fn read_ast<P: AsRef<Path>>(&self, input_path: P) -> Box<Read> {
    let input_ext = String::from(
      input_path
        .as_ref()
        .extension()
        .expect("input must have extension - can't determine language")
        .to_string_lossy(),
    );
    let raw_input = File::open(input_path).expect("failed to open input file");
    if input_ext == "tast" {
      return Box::new(raw_input);
    } else {
      let input_lang = self
        .lang_with_ext(&input_ext)
        .expect(format!("unsupported input language: {}", input_ext).as_str());
      return Box::new(input_lang.encode_ast(raw_input));
    }
  }

  pub fn write_ast(&self, output_path: PathBuf) -> (Box<Write>, AtomicFileCommit) {
    let output_ext = String::from(
      output_path
        .extension()
        .expect("output must have extension - can't determine language")
        .to_string_lossy(),
    );
    let raw_output =
      AtomicFile::create(output_path).expect("can't open temporary atomic output file");
    if output_ext == "tast" {
      return (Box::new(raw_output.temp_file), raw_output.commit);
    } else {
      let output_lang = self
        .lang_with_ext(&output_ext)
        .expect(format!("unsupported output language: {}", output_ext).as_str());
      return (
        Box::new(output_lang.decode_ast(raw_output.temp_file)),
        raw_output.commit,
      );
    }
  }

  pub fn start(&mut self, path: &Option<PathBuf>) {
    let mut libs_dir = self.root_dir.clone();
    libs_dir.push("libraries");
    for lib in self.libs.values_mut() {
      lib.start(&path, &libs_dir);
    }
  }

  fn call_fun(&mut self, lib: &String, name: String, args: Vec<Value>) -> Value {
    let lib = self
      .libs
      .get_mut(lib)
      .expect(format!("failed to call function - library not setup: {}", lib).as_str());
    return lib.call_fun(name, args);
  }

  pub fn call_fun_val(&mut self, head: String, args: Vec<Value>) -> Value {
    if let Some((lib, name)) = Value::record_head_to_fun(&head) {
      return self.call_fun(&lib, name, args);
    } else {
      panic!(
        "Can't extract function lib and name from record head: {}",
        head
      )
    }
  }

  pub fn stop(&mut self) {
    for lib in self.libs.values_mut() {
      lib.stop();
    }
  }
}