knave 0.14.21

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use super::exit_status::ExitStatus;
use super::source_report::SourceReport;
use super::stage_work::StageWork;
use crate::check::ProgramChecker;
use crate::diagnostic::Diagnostic;
use crate::expand::Program;
use crate::types::TypedProgram;

pub struct Checking<'a> {
    report: SourceReport<'a>,
    work: &'a mut StageWork,
}

impl<'a> Checking<'a> {
    pub fn new(report: SourceReport<'a>, work: &'a mut StageWork) -> Self {
        Self { report, work }
    }

    pub fn check(&mut self, program: &Program) -> Result<TypedProgram, ExitStatus> {
        self.check_showing(program, |_| true)
    }

    pub fn check_silently(&mut self, program: &Program) -> Result<TypedProgram, ExitStatus> {
        self.check_showing(program, |_| false)
    }

    pub fn check_showing(
        &mut self,
        program: &Program,
        shown_warning: impl FnMut(&Diagnostic) -> bool,
    ) -> Result<TypedProgram, ExitStatus> {
        let mut typed = ProgramChecker::check(program)
            .map_err(|diagnostics| self.report.report(&diagnostics))?;
        self.work.inferred_expressions = Some(typed.checking.inferred_expressions);
        typed.warnings.retain(shown_warning);
        self.report.print(&typed.warnings);
        Ok(typed)
    }
}

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

    use super::Checking;
    use crate::cli::exit_status::ExitStatus;
    use crate::cli::expansion::Expansion;
    use crate::cli::fake_programs::FakePrograms;
    use crate::cli::program_file::ProgramFile;
    use crate::cli::recorded_output::RecordedOutput;
    use crate::cli::source_report::SourceReport;
    use crate::cli::stage_work::StageWork;
    use crate::diagnostic::{Diagnostic, DiagnosticCode};
    use crate::expand::Program;
    use crate::source::SourceMap;

    struct CheckingFixture {
        sources: SourceMap,
        work: StageWork,
        output: RecordedOutput,
    }

    impl CheckingFixture {
        const MODULE: &str = "geometry.knv";
        const TWO_WARNINGS: &str = "(enum shape (circle f64) point)\n\n(defn area (s)\n  (match s (_ 0.0) (point 1.0)))\n\n(defn add-one (items) (push! items 1))\n";

        fn new() -> Self {
            Self {
                sources: SourceMap::default(),
                work: StageWork::default(),
                output: RecordedOutput::default(),
            }
        }

        fn expanded(&mut self, text: &str) -> Program {
            let file = ProgramFile::beside_itself(PathBuf::from(Self::MODULE));
            let entry = self.sources.add(Self::MODULE, text);
            Expansion::new(&mut self.sources, &mut self.work, &self.output)
                .expand(&file, entry)
                .expect("the fake module expands")
        }

        fn is_unreachable_arm(warning: &Diagnostic) -> bool {
            warning.code() == Some(&DiagnosticCode::UNREACHABLE_ARM)
        }

        fn checking(&mut self) -> Checking<'_> {
            Checking::new(
                SourceReport::new(&self.sources, &self.output),
                &mut self.work,
            )
        }
    }

    #[test]
    fn checks_a_program_and_records_its_inferred_expressions() {
        let mut fixture = CheckingFixture::new();
        let program = fixture.expanded(FakePrograms::WITHOUT_MAIN);
        assert!(fixture.checking().check(&program).is_ok());
        assert!(
            fixture
                .work
                .inferred_expressions
                .is_some_and(|count| count > 0)
        );
    }

    #[test]
    fn reports_a_program_that_does_not_check() {
        let mut fixture = CheckingFixture::new();
        let program = fixture.expanded(FakePrograms::MISTYPED);
        let status = fixture.checking().check(&program).err();
        assert_eq!(status, Some(ExitStatus::ProgramErrors));
        assert_eq!(fixture.output.writes(), ["1 diagnostics"]);
    }

    #[test]
    fn prints_every_warning_of_a_checked_program() {
        let mut fixture = CheckingFixture::new();
        let program = fixture.expanded(FakePrograms::UNREACHABLE_ARM);
        let typed = fixture
            .checking()
            .check(&program)
            .expect("the program checks");
        assert_eq!(typed.warnings.len(), 1);
        assert_eq!(fixture.output.writes(), ["1 diagnostics"]);
    }

    #[test]
    fn shows_only_the_warnings_it_is_asked_to_show() {
        let mut fixture = CheckingFixture::new();
        let program = fixture.expanded(CheckingFixture::TWO_WARNINGS);
        let typed = fixture
            .checking()
            .check_showing(&program, CheckingFixture::is_unreachable_arm)
            .expect("the program checks");
        let codes: Vec<&str> = typed
            .warnings
            .in_source_order()
            .into_iter()
            .filter_map(|warning| warning.code().map(DiagnosticCode::as_str))
            .collect();
        assert_eq!(codes, ["L0434"]);
        assert_eq!(fixture.output.writes(), ["1 diagnostics"]);
    }

    #[test]
    fn checks_silently_without_printing_a_warning() {
        let mut fixture = CheckingFixture::new();
        let program = fixture.expanded(FakePrograms::UNREACHABLE_ARM);
        let typed = fixture
            .checking()
            .check_silently(&program)
            .expect("the program checks");
        assert!(typed.warnings.is_empty());
        assert!(fixture.output.writes().is_empty());
    }
}