knave-expand 0.9.3

The reading, scoping and expansion stages of knave
Documentation
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use crate::core::ModulePath;
use crate::language::Language;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BuiltinModule {
    Kernel,
    Code,
}

pub enum ModuleSource {
    Builtin(BuiltinModule),
    Text { name: String, text: String },
}

pub struct ModuleSources {
    root: PathBuf,
    display_root: PathBuf,
}

impl ModuleSources {
    pub const KERNEL: &str = "kernel";
    pub const CODE: &str = "code";
    pub const PRELUDE: &str = "prelude";
    pub const TEST: &str = "prelude/test";
    const PRELUDE_DIRECTORY: &str = "prelude/";
    const BYTE_ORDER_MARK: char = '\u{feff}';
    const PRELUDE_FILES: [(&str, &str); 20] = [
        ("prelude", include_str!("../../prelude/prelude.rslisp")),
        (
            "prelude/macros",
            include_str!("../../prelude/macros.rslisp"),
        ),
        ("prelude/logic", include_str!("../../prelude/logic.rslisp")),
        (
            "prelude/syntax",
            include_str!("../../prelude/syntax.rslisp"),
        ),
        (
            "prelude/number",
            include_str!("../../prelude/number.rslisp"),
        ),
        ("prelude/text", include_str!("../../prelude/text.rslisp")),
        (
            "prelude/struct",
            include_str!("../../prelude/struct.rslisp"),
        ),
        (
            "prelude/ownership",
            include_str!("../../prelude/ownership.rslisp"),
        ),
        ("prelude/types", include_str!("../../prelude/types.rslisp")),
        ("prelude/pair", include_str!("../../prelude/pair.rslisp")),
        ("prelude/assoc", include_str!("../../prelude/assoc.rslisp")),
        ("prelude/place", include_str!("../../prelude/place.rslisp")),
        ("prelude/vec", include_str!("../../prelude/vec.rslisp")),
        (
            "prelude/iterator",
            include_str!("../../prelude/iterator.rslisp"),
        ),
        (
            "prelude/option",
            include_str!("../../prelude/option.rslisp"),
        ),
        (
            "prelude/result",
            include_str!("../../prelude/result.rslisp"),
        ),
        ("prelude/io", include_str!("../../prelude/io.rslisp")),
        ("prelude/map", include_str!("../../prelude/map.rslisp")),
        (
            "prelude/sequence",
            include_str!("../../prelude/sequence.rslisp"),
        ),
        ("prelude/test", include_str!("../../prelude/test.rslisp")),
    ];

    pub fn new(root: PathBuf, display_root: PathBuf) -> Self {
        Self { root, display_root }
    }

    pub fn is_prelude(path: &ModulePath) -> bool {
        path.as_str() == Self::PRELUDE || path.as_str().starts_with(Self::PRELUDE_DIRECTORY)
    }

    pub fn is_embedded_prelude(path: &ModulePath) -> bool {
        Self::PRELUDE_FILES
            .iter()
            .any(|(module, _)| *module == path.as_str())
    }

    pub fn is_written(path: &ModulePath) -> bool {
        !Self::is_prelude(path) && !Self::is_compiler_provided(path)
    }

    pub fn is_compiler_provided(path: &ModulePath) -> bool {
        [Self::KERNEL, Self::CODE].contains(&path.as_str())
    }

    pub fn relative_name<'n>(&self, name: &'n str) -> &'n str {
        Path::new(name)
            .strip_prefix(&self.display_root)
            .ok()
            .and_then(Path::to_str)
            .unwrap_or(name)
    }

    pub fn read_file(path: &Path) -> io::Result<String> {
        fs::read_to_string(path).map(Self::without_byte_order_mark)
    }

    pub fn read_standard_input() -> io::Result<String> {
        io::read_to_string(io::stdin()).map(Self::without_byte_order_mark)
    }

    fn without_byte_order_mark(mut text: String) -> String {
        if text.starts_with(Self::BYTE_ORDER_MARK) {
            text.drain(..Self::BYTE_ORDER_MARK.len_utf8());
        }
        text
    }

    pub fn find(&self, path: &ModulePath) -> Option<ModuleSource> {
        self.builtin(path)
            .or_else(|| self.prelude_file(path))
            .or_else(|| self.user_file(path))
    }

    fn builtin(&self, path: &ModulePath) -> Option<ModuleSource> {
        match path.as_str() {
            Self::KERNEL => Some(ModuleSource::Builtin(BuiltinModule::Kernel)),
            Self::CODE => Some(ModuleSource::Builtin(BuiltinModule::Code)),
            _ => None,
        }
    }

    fn prelude_file(&self, path: &ModulePath) -> Option<ModuleSource> {
        Self::PRELUDE_FILES
            .iter()
            .find(|(module, _)| *module == path.as_str())
            .map(|(_, text)| ModuleSource::Text {
                name: Self::file_name(path),
                text: (*text).to_string(),
            })
    }

    fn user_file(&self, path: &ModulePath) -> Option<ModuleSource> {
        let file_name = Self::file_name(path);
        let text = Self::read_file(&self.root.join(&file_name)).ok()?;
        Some(ModuleSource::Text {
            name: self.display_root.join(&file_name).display().to_string(),
            text,
        })
    }

    fn file_name(path: &ModulePath) -> String {
        format!("{path}.{}", Language::EXTENSION)
    }
}