knave 0.13.3

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use crate::diagnostic::{Diagnostic, Label};
use crate::source::SourceId;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModuleLoader {
    pub file: String,
    pub input: SourceId,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Location {
    Entered,
    Buffered(SourceId),
}

#[derive(Clone)]
pub struct InputLocator {
    buffered: Vec<SourceId>,
    entered: String,
    loaders: Vec<ModuleLoader>,
}

impl InputLocator {
    pub fn new(buffered: Vec<SourceId>, entered: String, loaders: Vec<ModuleLoader>) -> Self {
        Self {
            buffered,
            entered,
            loaders,
        }
    }

    pub fn locate(
        &self,
        diagnostic: &Diagnostic,
        file_of: impl Fn(SourceId) -> String,
    ) -> Location {
        let is_entered = |source| file_of(source) == self.entered;
        if Self::involves(diagnostic, is_entered) {
            return Location::Entered;
        }
        let primary = diagnostic.primary().span().source();
        Self::outermost_call(diagnostic)
            .and_then(|call| self.buffered_holding(call.span().source()))
            .or_else(|| self.buffered_holding(primary))
            .or_else(|| {
                self.loader_of(&file_of(primary))
                    .filter(|input| !is_entered(*input))
            })
            .map_or(Location::Entered, Location::Buffered)
    }

    fn involves(diagnostic: &Diagnostic, is_entered: impl Fn(SourceId) -> bool) -> bool {
        is_entered(diagnostic.primary().span().source())
            || Self::calls(diagnostic).any(|call| is_entered(call.span().source()))
    }

    fn outermost_call(diagnostic: &Diagnostic) -> Option<&Label> {
        Self::calls(diagnostic).last()
    }

    fn calls(diagnostic: &Diagnostic) -> impl Iterator<Item = &Label> {
        diagnostic
            .labels()
            .filter(|label| label.marks_a_macro_call())
    }

    fn buffered_holding(&self, source: SourceId) -> Option<SourceId> {
        self.buffered.contains(&source).then_some(source)
    }

    fn loader_of(&self, file: &str) -> Option<SourceId> {
        self.loaders
            .iter()
            .find(|loader| loader.file == file)
            .map(|loader| loader.input)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::diagnostic::DiagnosticCode;
    use crate::expand::ExpandProblems;
    use crate::source::Span;

    struct LocationFixture;

    impl LocationFixture {
        const EARLIER: usize = 1;
        const LATER: usize = 2;
        const ENTERED: usize = 3;
        const MODULE: usize = 4;
        const PRELUDE: usize = 5;
        const ENTERED_AGAIN: usize = 6;
        const MODULE_FILE: &str = "shapes.knv";

        fn source(index: usize) -> SourceId {
            SourceId::new(index)
        }

        fn span(index: usize) -> Span {
            Span::new(Self::source(index), 0..1)
        }

        fn file_of(source: SourceId) -> String {
            match source.index() {
                Self::MODULE => Self::MODULE_FILE.to_string(),
                Self::ENTERED_AGAIN => Self::file_of(Self::source(Self::ENTERED)),
                index => format!("<source {index}>"),
            }
        }

        fn locator(loader: usize) -> InputLocator {
            InputLocator::new(
                vec![Self::source(Self::EARLIER), Self::source(Self::LATER)],
                Self::file_of(Self::source(Self::ENTERED)),
                vec![ModuleLoader {
                    file: Self::MODULE_FILE.to_string(),
                    input: Self::source(loader),
                }],
            )
        }

        fn error_at(index: usize) -> Diagnostic {
            Diagnostic::error(
                DiagnosticCode::UNBOUND_NAME,
                "an error",
                Label::new(Self::span(index), "here"),
            )
        }

        fn called_from(error: Diagnostic, index: usize) -> Diagnostic {
            error.with_label(ExpandProblems::macro_call(Self::span(index)))
        }

        fn locate(diagnostic: &Diagnostic) -> Location {
            Self::locator(Self::LATER).locate(diagnostic, Self::file_of)
        }

        fn buffered(index: usize) -> Location {
            Location::Buffered(Self::source(index))
        }
    }

    #[test]
    fn an_error_whose_primary_label_lies_in_the_entered_input_is_its_own() {
        let error = LocationFixture::called_from(
            LocationFixture::error_at(LocationFixture::ENTERED),
            LocationFixture::EARLIER,
        );
        assert_eq!(LocationFixture::locate(&error), Location::Entered);
    }

    #[test]
    fn an_error_called_from_the_entered_input_is_its_own() {
        let error = LocationFixture::called_from(
            LocationFixture::error_at(LocationFixture::EARLIER),
            LocationFixture::ENTERED,
        );
        assert_eq!(LocationFixture::locate(&error), Location::Entered);
    }

    #[test]
    fn the_entered_input_is_known_by_its_name_in_another_source() {
        let error = LocationFixture::called_from(
            LocationFixture::error_at(LocationFixture::EARLIER),
            LocationFixture::ENTERED_AGAIN,
        );
        assert_eq!(LocationFixture::locate(&error), Location::Entered);
    }

    #[test]
    fn an_error_lies_at_the_input_of_its_outermost_macro_call() {
        let inner = LocationFixture::called_from(
            LocationFixture::error_at(LocationFixture::EARLIER),
            LocationFixture::EARLIER,
        );
        let error = LocationFixture::called_from(inner, LocationFixture::LATER);
        assert_eq!(
            LocationFixture::locate(&error),
            LocationFixture::buffered(LocationFixture::LATER)
        );
    }

    #[test]
    fn a_macro_call_outside_every_input_leaves_the_primary_label_to_decide() {
        let error = LocationFixture::called_from(
            LocationFixture::error_at(LocationFixture::EARLIER),
            LocationFixture::PRELUDE,
        );
        assert_eq!(
            LocationFixture::locate(&error),
            LocationFixture::buffered(LocationFixture::EARLIER)
        );
    }

    #[test]
    fn an_error_lies_at_the_input_of_its_primary_label() {
        let error = LocationFixture::error_at(LocationFixture::EARLIER).with_label(Label::new(
            LocationFixture::span(LocationFixture::ENTERED),
            "first",
        ));
        assert_eq!(
            LocationFixture::locate(&error),
            LocationFixture::buffered(LocationFixture::EARLIER)
        );
    }

    #[test]
    fn an_error_in_a_module_file_lies_at_the_input_that_loaded_it() {
        let error = LocationFixture::error_at(LocationFixture::MODULE);
        assert_eq!(
            LocationFixture::locate(&error),
            LocationFixture::buffered(LocationFixture::LATER)
        );
    }

    #[test]
    fn an_error_in_a_module_the_entered_input_loaded_is_its_own() {
        let error = LocationFixture::error_at(LocationFixture::MODULE);
        let location = LocationFixture::locator(LocationFixture::ENTERED)
            .locate(&error, LocationFixture::file_of);
        assert_eq!(location, Location::Entered);
    }

    #[test]
    fn an_error_located_at_no_input_is_the_entered_inputs() {
        let error = LocationFixture::error_at(LocationFixture::PRELUDE);
        assert_eq!(LocationFixture::locate(&error), Location::Entered);
    }
}