knave-expand 0.13.2

The reading, scoping and expansion stages of knave
Documentation
use crate::interpret::{ListConversion, PassedLimit, StepBudget, Value};
use crate::reader::Form;
use crate::scope::Scope;

#[derive(Clone)]
pub struct FormBudget {
    remaining: usize,
    steps: StepBudget,
}

impl FormBudget {
    pub const EXPANSION_LIMIT: usize = ListConversion::FORM_LIMIT;
    const NO_ENCLOSING_LISTS: usize = 0;

    pub fn full() -> Self {
        Self {
            remaining: Self::EXPANSION_LIMIT,
            steps: StepBudget::full(),
        }
    }

    pub fn steps(&self) -> StepBudget {
        self.steps.clone()
    }

    pub fn exhaust(&mut self) {
        self.remaining = 0;
    }

    pub fn is_exhausted(&self) -> bool {
        self.remaining == 0
    }

    pub fn admitted_form(&self, value: &Value) -> Result<Form, PassedLimit> {
        let mut allowance = self.remaining;
        value.to_form_within(&mut allowance, Self::NO_ENCLOSING_LISTS)
    }

    pub fn flip_scope(&mut self, form: &mut Form, scope: Scope, counted_forms: usize) -> bool {
        let mut allowance = self.remaining.saturating_add(counted_forms);
        let flipped = form.flip_scope_within(scope, &mut allowance);
        self.remaining = if flipped {
            allowance.min(self.remaining)
        } else {
            0
        };
        flipped
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::interpret::CodeList;
    use crate::reader::{FormKind, Symbol};
    use crate::scope::ScopeAllocator;
    use crate::source::{SourceId, Span};

    struct BudgetFixture {
        budget: FormBudget,
        scopes: ScopeAllocator,
    }

    impl BudgetFixture {
        const NOTHING_COUNTED: usize = 0;

        fn new() -> Self {
            Self {
                budget: FormBudget::full(),
                scopes: ScopeAllocator::default(),
            }
        }

        fn admits_list_of(&mut self, forms: usize) -> bool {
            let mut list = Self::list_of(forms);
            let scope = self.scopes.fresh();
            self.budget
                .flip_scope(&mut list, scope, Self::NOTHING_COUNTED)
        }

        fn admits_list_counting(&mut self, forms: usize, counted_forms: usize) -> bool {
            let mut list = Self::list_of(forms);
            let scope = self.scopes.fresh();
            self.budget.flip_scope(&mut list, scope, counted_forms)
        }

        fn admits_value(&self, value: &Value) -> bool {
            self.budget.admitted_form(value).is_ok()
        }

        fn list_of(forms: usize) -> Form {
            let items = vec![Self::leaf(); forms - 1];
            Form::new(FormKind::List(items), Self::span())
        }

        fn code_list(items: Vec<Value>) -> Value {
            Value::List(CodeList::new(items, Self::span()).expect("within the limit"))
        }

        fn leaf() -> Form {
            Form::new(FormKind::Symbol(Symbol::new("leaf")), Self::span())
        }

        fn span() -> Span {
            Span::new(SourceId::new(0), 0..0)
        }
    }

    #[test]
    fn admits_an_expansion_of_exactly_the_limit() {
        let mut fixture = BudgetFixture::new();
        assert!(fixture.admits_list_of(FormBudget::EXPANSION_LIMIT));
    }

    #[test]
    fn rejects_an_expansion_one_form_past_the_limit() {
        let mut fixture = BudgetFixture::new();
        assert!(!fixture.admits_list_of(FormBudget::EXPANSION_LIMIT + 1));
    }

    #[test]
    fn admits_counted_forms_beyond_the_limit() {
        let mut fixture = BudgetFixture::new();
        assert!(fixture.admits_list_counting(FormBudget::EXPANSION_LIMIT + 1, 1));
        assert!(!fixture.admits_list_of(1));
    }

    #[test]
    fn gains_no_budget_from_counted_forms_it_did_not_return() {
        let mut fixture = BudgetFixture::new();
        assert!(fixture.admits_list_counting(1, FormBudget::EXPANSION_LIMIT));
        assert!(fixture.admits_list_of(FormBudget::EXPANSION_LIMIT));
        assert!(!fixture.admits_list_of(1));
    }

    #[test]
    fn spends_the_rest_of_the_budget_on_a_failed_expansion() {
        let mut fixture = BudgetFixture::new();
        assert!(!fixture.admits_list_counting(FormBudget::EXPANSION_LIMIT + 2, 1));
        assert!(!fixture.admits_list_of(1));
    }

    #[test]
    fn counts_the_forms_of_every_expansion_against_one_limit() {
        let mut fixture = BudgetFixture::new();
        let half = FormBudget::EXPANSION_LIMIT / 2;
        assert!(fixture.admits_list_of(half));
        assert!(fixture.admits_list_of(half));
        assert!(!fixture.admits_list_of(1));
    }

    #[test]
    fn admits_a_converted_list_of_exactly_the_limit() {
        let leaves = vec![Value::Form(BudgetFixture::leaf()); FormBudget::EXPANSION_LIMIT - 1];
        let list = BudgetFixture::code_list(leaves);
        assert!(BudgetFixture::new().admits_value(&list));
    }

    #[test]
    fn rejects_a_converted_list_whose_leaves_pass_the_limit() {
        let leaves = vec![Value::Form(BudgetFixture::leaf()); FormBudget::EXPANSION_LIMIT];
        let list = BudgetFixture::code_list(leaves);
        assert!(!BudgetFixture::new().admits_value(&list));
    }

    #[test]
    fn rejects_a_converted_list_whose_inner_lists_pass_the_limit() {
        let empty_lists = vec![BudgetFixture::code_list(Vec::new()); FormBudget::EXPANSION_LIMIT];
        let list = BudgetFixture::code_list(empty_lists);
        assert!(!BudgetFixture::new().admits_value(&list));
    }

    #[test]
    fn a_cloned_budget_spends_from_the_same_steps() {
        let budget = FormBudget::full();
        budget.steps().set_limit(1);
        let restored = budget.clone();
        assert!(restored.steps().spend().is_ok());
        assert!(budget.steps().spend().is_err());
    }

    #[test]
    fn a_full_budget_is_not_exhausted() {
        assert!(!FormBudget::full().is_exhausted());
    }

    #[test]
    fn spending_every_form_exhausts_the_budget() {
        let mut fixture = BudgetFixture::new();
        assert!(fixture.admits_list_of(FormBudget::EXPANSION_LIMIT));
        assert!(fixture.budget.is_exhausted());
    }

    #[test]
    fn an_exhausted_budget_admits_no_form() {
        let mut fixture = BudgetFixture::new();
        fixture.budget.exhaust();
        assert!(fixture.budget.is_exhausted());
        assert!(!fixture.admits_list_of(1));
    }
}