quail 0.1.0

The Quail Programming Language
use std::collections::HashMap;

use dirs;
use rustyline::error::ReadlineError;

use crate::parser;
use crate::ast;
use crate::hole;
use crate::builtins;
use crate::typecheck;

use ast::Term;
use ast::TermNode;
use ast::Def;
use ast::Value;
use ast::Context;
use ast::HoleId;
use ast::Import;
use ast::MatchArm;
use builtins::InductiveTypeDef;
use typecheck::TypeContext;

#[derive(Debug)]
pub struct Runtime {
    pub imports: Vec<String>,
    pub holes: HashMap<HoleId, Value>,
    pub readline_file: String,
    pub editor: rustyline::Editor<()>,
    pub number_of_holes: u64,

    pub inductive_typedefs: HashMap<String, InductiveTypeDef>,

    pub definition_ctx: Context,
    pub definition_type_ctx: TypeContext,

    pub builtin_ctx: Context,
    pub builtin_type_ctx: TypeContext,
}

impl Runtime {
    pub fn new() -> Self {
        let readline_file = dirs::config_dir()
            .expect("User does not have a home directory??")
            .join("quail").join("history");

        if !readline_file.exists() {
            std::fs::create_dir_all(&readline_file.parent().unwrap()).unwrap();
            std::fs::File::create(&readline_file).expect("Could not create readline file");
        }

        let inductive_typedefs: HashMap<String, InductiveTypeDef> = builtins::builtin_inductive_typedefs()
            .iter()
            .map(|itd| (itd.name.to_string(), itd.clone()))
            .collect();

        let mut builtin_ctx = builtins::builtins_ctx();
        let mut builtin_type_ctx = builtins::builtins_type_ctx();

        for inductive_typedef in inductive_typedefs.values() {
            builtin_ctx = builtin_ctx.append(inductive_typedef.ctor_context());
            builtin_type_ctx = builtin_type_ctx.append(inductive_typedef.ctor_type_context());
        }

        let mut editor = rustyline::Editor::new();
        if editor.load_history(&readline_file).is_err() {
            eprintln!("Could not read from {:?} for readline history.", &readline_file);
        }

        Runtime {
            imports: vec![],
            holes: HashMap::new(),
            readline_file: readline_file.to_string_lossy().to_string(),
            editor,
            number_of_holes: 0,

            inductive_typedefs,

            definition_ctx: Context::empty(),
            builtin_ctx,

            definition_type_ctx: TypeContext::empty(),
            builtin_type_ctx,
        }
    }

    pub fn import(&mut self, name: &str) -> Result<(), RuntimeError> {
        let mut import_filename = name.to_string();
        import_filename.push_str(".ql");
        let import_filename = &std::path::Path::new(&import_filename);

        match std::env::current_dir() {
            Ok(basedir) => self.load_module(import_filename, &basedir, false),
            Err(err) => Err(err.into()),
        }
    }

    pub fn load(&mut self, filepath: impl AsRef<std::path::Path>) -> Result<(), RuntimeError> {
        let basedir = std::path::Path::new(filepath.as_ref().parent().expect("Invalid path"));
        let filename = std::path::Path::new(filepath.as_ref().file_name().expect("Invalid path"));
        self.load_module(filename, basedir, true)
    }

    fn load_module(
        &mut self,
        filename: &std::path::Path,
        basedir: &std::path::Path,
        is_main: bool,
    ) -> Result<(), RuntimeError> {
        if self.imports.contains(&filename.to_string_lossy().to_string()) {
            return Ok(());
        } else {
            self.imports.push(filename.to_string_lossy().to_string());
        }
        let filepath = basedir.join(filename);
        use std::fs;
        use std::io::Read;
        let mut module_text = String::new();
        fs::File::open(&filepath)?.read_to_string(&mut module_text)?;

        let (module, number_of_new_holes) = parser::parse_module(self.next_hole_id(), Some(filename), &module_text)?;
        self.add_holes(number_of_new_holes);

        for import in module.imports {
            let Import(name) = import;

            let mut import_filename = name.to_string();
            import_filename.push_str(".ql");
            let import_filename = &std::path::Path::new(&import_filename);

            self.load_module(import_filename, basedir, false)?;
        }

        for definition in module.definitions.iter() {
            let Def(name, typ, body) = definition;
            if is_main || name != "main" {
                let type_context = self.builtin_type_ctx.append(self.definition_type_ctx.clone()).extend(name, typ.clone());
                typecheck::check_type(body.clone(), type_context, &self.inductive_typedefs, typ.clone())?;
                self.definition_type_ctx = self.definition_type_ctx.extend(&name.to_string(), typ.clone());

                let body_value = self.eval(body.clone(), Context::empty());
                self.definition_ctx = self.definition_ctx.extend(&name.to_string(), body_value);
            }
        }
        Ok(())
    }

    pub fn define(&mut self, definition: &Def) -> Result<(), RuntimeError> {
        let Def(name, typ, body) = definition;
        let type_context = self.builtin_type_ctx.append(self.definition_type_ctx.clone()).extend(&name, typ.clone());
        typecheck::check_type(body.clone(), type_context, &self.inductive_typedefs, typ.clone())?;
        self.definition_type_ctx = self.definition_type_ctx.extend(&name.to_string(), typ.clone());

        let body_value = self.eval(body.clone(), Context::empty());
        self.definition_ctx = self.definition_ctx.extend(&name.to_string(), body_value);
        Ok(())
    }

    pub fn next_hole_id(&self) -> HoleId {
        self.number_of_holes as HoleId
    }

    pub fn add_holes(&mut self, number_of_holes: u64) {
        self.number_of_holes += number_of_holes;
    }

    pub fn exec(&mut self) {
        self.definition_ctx.lookup("main").expect("There should be a main in your module");
    }

    pub fn readline(&mut self) -> Result<String, ReadlineError> {
        let line = self.editor.readline("> ")?;
        self.editor.add_history_entry(line.as_str());
        self.editor.save_history(&self.readline_file)?;
        Ok(line)
    }

    pub fn fill_hole(&mut self, hole_id: HoleId, value: Value) {
        self.holes.insert(hole_id, value);
    }

    pub fn hole_value(&self, hole_id: HoleId) -> Option<Value> {
        assert!((hole_id as u64) < self.number_of_holes, "Invalid HoleId!");
        self.holes.get(&hole_id).cloned()
    }

    fn apply(self: &mut Runtime, func: Value, args: Vec<Value>) -> Value {
        match &func {
            Value::Fun(x, body, local_ctx) => {
                match args.clone().split_first() {
                    None => func,
                    Some((v, vs_remaining)) => {
                        let new_ctx = local_ctx.extend(&x, v.clone());
                        let new_func = self.eval(body.clone(), new_ctx);
                        self.apply(new_func, vs_remaining.to_vec())
                    },
                }
            },
            Value::Ctor(tag, contents) => {
                let mut new_contents = contents.clone();
                new_contents.extend(args);
                Value::Ctor(tag.to_string(), new_contents)
            },
            Value::Prim(prim) => {
                prim(args)
            },
            _ => panic!(format!("Applied arguments to non-function {:?}", func)),
        }
    }

    #[allow(mutable_borrow_reservation_conflict)]
    pub fn eval(self: &mut Runtime, t: Term, ctx: Context) -> Value {
        match t.as_ref() {
            TermNode::Var(x) => {
                ctx.lookup(&x)
                    .or_else(|| self.definition_ctx.lookup(&x))
                    .or_else(|| self.builtin_ctx.lookup(&x))
                    .expect(&format!("Unbound variable {:?}", &x))
            },
            TermNode::StrLit(contents) => Value::Str(contents.to_string()),
            TermNode::Lam(x, body) => {
                Value::Fun(x.clone(), body.clone(), ctx.clone())
            },
            TermNode::App(f, vs) => {
                let f_value = self.eval(f.clone(), ctx.clone());
                let vs_values: Vec<Value> = vs.iter().map(|v| self.eval(v.clone(), ctx.clone())).collect();
                self.apply(f_value, vs_values)
            },
            TermNode::Let(x, v, body) => {
                let v_value = self.eval(v.clone(), ctx.clone());
                let extended_ctx = ctx.extend(x, v_value);
                self.eval(body.clone(), extended_ctx)
            },
            TermNode::Match(t, match_arms) => {
                let t_value = self.eval(t.clone(), ctx.clone());
                match t_value {
                    Value::Ctor(tag, contents) => {
                        let MatchArm(pat, body) = ast::find_matching_arm(&tag, &match_arms);

                        let bind_names: Vec<String> = pat[1..].to_vec();
                        let bind_values: Vec<Value> = contents.clone();
                        let bindings: Vec<(String, Value)> = bind_names.into_iter().zip(bind_values).collect();

                        let extended_ctx = ctx.extend_many(&bindings);
                        self.eval(body, extended_ctx)
                    },
                    _ => panic!(format!("Expected a constructor during match statement, but found {:?}", &t_value)),
                }
            },
            TermNode::Hole(hole_info) => hole::fill(self, hole_info, ctx),
            TermNode::As(term, _typ) => self.eval(term.clone(), ctx),
        }
    }
}

#[derive(Debug)]
pub struct RuntimeError(String);

impl std::convert::From<std::io::Error> for RuntimeError {
    fn from(error: std::io::Error) -> Self {
        RuntimeError(error.to_string())
    }
}

impl std::convert::From<String> for RuntimeError {
    fn from(error: String) -> Self {
        RuntimeError(error)
    }
}