knave 0.12.3

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use std::rc::Rc;

use rustc_hash::FxHashMap;

use crate::core::Namespace;
use crate::diagnostic::MissingName;
use crate::expand::{BoundName, ExpandProblems};

#[derive(Clone, Default)]
pub struct NameRecords {
    records: FxHashMap<(Namespace, Rc<str>), NameRecord>,
}

#[derive(Clone)]
struct NameRecord {
    setter: String,
    cause: RecordCause,
}

#[derive(Clone)]
enum RecordCause {
    SetAside { reason: String },
    NoLongerDefined { input: String },
}

#[derive(Clone, Copy)]
pub enum Tense {
    Done,
    Pending,
}

impl NameRecords {
    pub fn set_aside(&mut self, name: &BoundName, setter: &str, reason: &str) {
        let cause = RecordCause::SetAside {
            reason: reason.to_string(),
        };
        self.record(name, setter, cause);
    }

    pub fn no_longer_defined(&mut self, name: &BoundName, input: &str, setter: &str) {
        let cause = RecordCause::NoLongerDefined {
            input: input.to_string(),
        };
        self.record(name, setter, cause);
    }

    pub fn absorb(&mut self, other: NameRecords) {
        self.records.extend(other.records);
    }

    pub fn forget<'a>(&mut self, bound: impl IntoIterator<Item = &'a BoundName>) {
        for name in bound {
            self.records
                .remove(&(name.namespace, Rc::clone(&name.name)));
        }
    }

    pub fn note(&self, missing: &MissingName, tense: Tense) -> Option<String> {
        let record = self
            .records
            .get(&(missing.namespace, Rc::clone(&missing.name)))?;
        Some(record.note(&missing.name, tense))
    }

    fn record(&mut self, name: &BoundName, setter: &str, cause: RecordCause) {
        let record = NameRecord {
            setter: setter.to_string(),
            cause,
        };
        self.records
            .insert((name.namespace, Rc::clone(&name.name)), record);
    }
}

impl NameRecord {
    fn note(&self, name: &str, tense: Tense) -> String {
        match (&self.cause, tense) {
            (RecordCause::SetAside { reason }, Tense::Done) => {
                ExpandProblems::set_aside_note(&self.setter, name, reason)
            }
            (RecordCause::SetAside { reason }, Tense::Pending) => {
                ExpandProblems::would_set_aside_note(&self.setter, name, reason)
            }
            (RecordCause::NoLongerDefined { input }, _) => {
                ExpandProblems::no_longer_defines_note(input, name, &self.setter)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::source::{SourceId, Span};

    struct RecordFixture;

    impl RecordFixture {
        fn bound(name: &str, namespace: Namespace) -> BoundName {
            BoundName {
                name: name.into(),
                namespace,
                span: Span::new(SourceId::new(0), 0..1),
            }
        }

        fn missing(name: &str, namespace: Namespace) -> MissingName {
            MissingName {
                name: name.into(),
                namespace,
            }
        }

        fn set_aside_karl() -> NameRecords {
            let mut records = NameRecords::default();
            records.set_aside(
                &Self::bound("karl", Namespace::Value),
                "<input 4>",
                "missing field `email` in `user`",
            );
            records
        }
    }

    #[test]
    fn a_set_aside_name_notes_its_setter_and_reason() {
        let note = RecordFixture::set_aside_karl().note(
            &RecordFixture::missing("karl", Namespace::Value),
            Tense::Done,
        );
        assert_eq!(
            note.as_deref(),
            Some("`<input 4>` set aside `karl`: missing field `email` in `user`")
        );
    }

    #[test]
    fn a_pending_set_aside_name_notes_what_would_happen() {
        let note = RecordFixture::set_aside_karl().note(
            &RecordFixture::missing("karl", Namespace::Value),
            Tense::Pending,
        );
        assert_eq!(
            note.as_deref(),
            Some("`<input 4>` would set aside `karl`: missing field `email` in `user`")
        );
    }

    #[test]
    fn a_name_of_another_namespace_has_no_note() {
        let note = RecordFixture::set_aside_karl().note(
            &RecordFixture::missing("karl", Namespace::Type),
            Tense::Done,
        );
        assert_eq!(note, None);
    }

    #[test]
    fn a_name_no_longer_defined_notes_the_input_that_stopped() {
        let mut records = NameRecords::default();
        records.no_longer_defined(
            &RecordFixture::bound("foo", Namespace::Value),
            "<input 3>",
            "<input 4>",
        );
        let note = records.note(
            &RecordFixture::missing("foo", Namespace::Value),
            Tense::Done,
        );
        assert_eq!(
            note.as_deref(),
            Some("`<input 3>` no longer defines `foo` after `<input 4>`")
        );
    }

    #[test]
    fn a_forgotten_name_has_no_note() {
        let mut records = RecordFixture::set_aside_karl();
        records.forget([&RecordFixture::bound("karl", Namespace::Value)]);
        let missing = RecordFixture::missing("karl", Namespace::Value);
        assert_eq!(records.note(&missing, Tense::Done), None);
    }

    #[test]
    fn absorbed_records_note_their_names() {
        let mut records = NameRecords::default();
        records.absorb(RecordFixture::set_aside_karl());
        let missing = RecordFixture::missing("karl", Namespace::Value);
        assert!(records.note(&missing, Tense::Done).is_some());
    }
}