knave 0.14.16

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use std::fmt::Display;
use std::path::Path;

use crate::cli::diagnostic_output::DiagnosticOutput;
use crate::cli::exit_status::ExitStatus;
use crate::cli::unreadable_file::UnreadableFile;
use crate::diagnostic::Diagnostics;
use crate::modules::ModuleSources;
use crate::project::{Project, ProjectReader};
use crate::reader::{Form, Reader};
use crate::source::{SourceId, SourceMap, Span, TextPosition};

pub struct SourceLoader<'a> {
    sources: &'a mut SourceMap,
    output: &'a dyn DiagnosticOutput,
}

impl<'a> SourceLoader<'a> {
    pub fn new(sources: &'a mut SourceMap, output: &'a dyn DiagnosticOutput) -> Self {
        Self { sources, output }
    }

    pub fn load(&mut self, path: &Path) -> Result<SourceId, ExitStatus> {
        match ModuleSources::read_file(path) {
            Ok(text) => Ok(self.add(path.display(), text)),
            Err(error) => Err(UnreadableFile::new(path, &error).report_to(self.output)),
        }
    }

    pub fn load_project(&mut self, path: &Path) -> Result<Project, ExitStatus> {
        let source = self.load(path)?;
        self.read_project(source)
    }

    pub fn add(&mut self, name: impl Display, text: impl Into<String>) -> SourceId {
        self.sources.add(name.to_string(), text)
    }

    pub fn add_inside(
        &mut self,
        enclosing: SourceId,
        position: TextPosition,
        text: &str,
    ) -> Option<SourceId> {
        self.sources.add_inside(enclosing, position, text)
    }

    pub fn read(&self, source: SourceId) -> Result<Vec<Form>, ExitStatus> {
        Reader::new(self.sources.file(source))
            .read()
            .map_err(|diagnostics| self.report(&diagnostics))
    }

    pub fn read_project(&self, source: SourceId) -> Result<Project, ExitStatus> {
        let forms = self.read(source)?;
        let whole_file = Span::new(source, 0..self.sources.file(source).text().len());
        ProjectReader::read(&forms, whole_file).map_err(|diagnostics| self.report(&diagnostics))
    }

    fn report(&self, diagnostics: &Diagnostics) -> ExitStatus {
        self.output.report(self.sources, diagnostics)
    }
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::{Path, PathBuf};

    use super::SourceLoader;
    use crate::cli::exit_status::ExitStatus;
    use crate::cli::recorded_output::RecordedOutput;
    use crate::cli::temporary_directory::TemporaryDirectory;
    use crate::language::Language;
    use crate::source::{SourceId, SourceMap, TextPosition};

    #[derive(Default)]
    struct LoaderFixture {
        sources: SourceMap,
        output: RecordedOutput,
    }

    impl LoaderFixture {
        const MODULE: &str = "geometry.knv";
        const MISSING: &str = "no-such-directory/missing.knv";

        fn loader(&mut self) -> SourceLoader<'_> {
            SourceLoader::new(&mut self.sources, &self.output)
        }

        fn added(&mut self, text: &str) -> SourceId {
            self.loader().add(Self::MODULE, text.to_string())
        }

        fn written_project_file(directory: &TemporaryDirectory, text: &str) -> PathBuf {
            let path = directory.path().join(Language::project_file_name());
            fs::write(&path, text).expect("the scratch directory is writable");
            path
        }

        fn reports(&self) -> Vec<String> {
            self.output.writes()
        }
    }

    #[test]
    fn reads_the_forms_of_an_added_text() {
        let mut fixture = LoaderFixture::default();
        let source = fixture.added("(area 2 3) (area 4 5)");
        let forms = fixture.loader().read(source).expect("the text reads");
        assert_eq!(forms.len(), 2);
    }

    #[test]
    fn names_an_added_text_by_its_path() {
        let mut fixture = LoaderFixture::default();
        let source = fixture.added("(area 2 3)");
        assert_eq!(fixture.sources.file(source).name(), LoaderFixture::MODULE);
    }

    #[test]
    fn names_a_named_text_by_its_name() {
        let mut fixture = LoaderFixture::default();
        let source = fixture.loader().add("<input 1>", "(area 2 3)");
        assert_eq!(fixture.sources.file(source).name(), "<input 1>");
    }

    #[test]
    fn reports_a_text_that_does_not_read() {
        let mut fixture = LoaderFixture::default();
        let source = fixture.added("(area 2 3");
        let status = fixture.loader().read(source);
        assert_eq!(status.err(), Some(ExitStatus::ProgramErrors));
        assert_eq!(fixture.reports(), ["1 diagnostics"]);
    }

    #[test]
    fn reports_a_missing_file_through_its_output() {
        let mut fixture = LoaderFixture::default();
        let status = fixture.loader().load(Path::new(LoaderFixture::MISSING));
        assert_eq!(status.err(), Some(ExitStatus::UsageError));
        let [report] = fixture.reports().try_into().expect("one report");
        assert!(report.starts_with("error: cannot read `no-such-directory/missing.knv`: "));
    }

    #[test]
    fn loads_a_project_from_its_file() {
        let directory = TemporaryDirectory::create().expect("a scratch directory is created");
        let path = LoaderFixture::written_project_file(&directory, "(defproject app)");
        let mut fixture = LoaderFixture::default();
        let project = fixture
            .loader()
            .load_project(&path)
            .expect("the project loads");
        assert_eq!(project.name, "app");
    }

    #[test]
    fn loads_no_project_from_a_missing_file() {
        let mut fixture = LoaderFixture::default();
        let status = fixture
            .loader()
            .load_project(Path::new(LoaderFixture::MISSING));
        assert_eq!(status.err(), Some(ExitStatus::UsageError));
        assert_eq!(fixture.reports().len(), 1);
    }

    #[test]
    fn adds_an_excerpt_found_at_its_position_under_its_module_name() {
        let mut fixture = LoaderFixture::default();
        let module = fixture.added("(area 2 3)\n(area 4 5)\n");
        let excerpt = fixture
            .loader()
            .add_inside(module, TextPosition::new(2, 1), "(area 4 5)")
            .expect("the excerpt is at its position");
        assert_eq!(fixture.sources.file(excerpt).name(), LoaderFixture::MODULE);
    }

    #[test]
    fn refuses_an_excerpt_missing_from_its_position() {
        let mut fixture = LoaderFixture::default();
        let module = fixture.added("(area 2 3)\n");
        let excerpt = fixture
            .loader()
            .add_inside(module, TextPosition::new(1, 1), "(area 4 5)");
        assert!(excerpt.is_none());
    }

    #[test]
    fn reads_a_project_from_its_project_form() {
        let mut fixture = LoaderFixture::default();
        let source = fixture.added("(defproject app)");
        let project = fixture
            .loader()
            .read_project(source)
            .expect("the project reads");
        assert_eq!(project.name, "app");
    }

    #[test]
    fn reports_a_project_file_without_a_project_form() {
        let mut fixture = LoaderFixture::default();
        let source = fixture.added("(area 2 3)");
        let status = fixture.loader().read_project(source);
        assert_eq!(status.err(), Some(ExitStatus::ProgramErrors));
        assert_eq!(fixture.reports(), ["1 diagnostics"]);
    }
}