knave-expand 0.14.21

The reading, scoping and expansion stages of knave
Documentation
use super::Expander;
use super::budget_arena::BudgetId;
use crate::diagnostic::Diagnostic;

impl Expander<'_> {
    pub(super) fn within_top_level_form<T>(
        &mut self,
        expand: impl FnOnce(&mut Self) -> Result<T, Diagnostic>,
    ) -> Result<T, Diagnostic> {
        let budget = self.form_budgets.open();
        self.within_source_form(budget, expand)
    }

    pub(super) fn within_source_form<T>(
        &mut self,
        budget: BudgetId,
        expand: impl FnOnce(&mut Self) -> Result<T, Diagnostic>,
    ) -> Result<T, Diagnostic> {
        let first_recovered = self.recovered.mark();
        let expanded = self.spending(budget, expand);
        self.recovered.settle_since(first_recovered, expanded)
    }

    pub(super) fn within_source_run(
        &mut self,
        budget: BudgetId,
        lower: impl FnOnce(&mut Self) -> Vec<Diagnostic>,
    ) {
        let first_recovered = self.recovered.mark();
        let failures = self.spending(budget, lower);
        self.recovered
            .keep_distinct_since(first_recovered, failures);
    }

    pub(super) fn spending<T>(
        &mut self,
        budget: BudgetId,
        expand: impl FnOnce(&mut Self) -> T,
    ) -> T {
        let outer = self.form_budgets.spending();
        self.form_budgets.spend_from(budget);
        let expanded = expand(self);
        self.form_budgets.spend_from(outer);
        expanded
    }
}

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

    use super::Expander;
    use crate::core::ModulePath;
    use crate::modules::ModuleSources;
    use crate::source::SourceMap;

    struct ExpressionFixture {
        sources: SourceMap,
    }

    impl ExpressionFixture {
        const ENTRY: &str = "entry";
        const SPINNING_MACRO: &str = "(use code number-gt number-sub)

(defmacro spin-steps (count)
  (local spin
    (fn (left)
      (if (number-gt left 0) (recur (number-sub left 1)) left)))
  (spin count))";
        const LOWERED_LIMIT: &str = "(with-macro-steps 20000 0)";
        const SPENDS_PAST_THE_LOWERED_LIMIT: &str = "(spin-steps 20000)";

        fn new() -> Self {
            Self {
                sources: SourceMap::default(),
            }
        }

        fn expands_both(&mut self, first: &str, second: &str) -> bool {
            let entry = self.sources.add(Self::ENTRY, Self::SPINNING_MACRO);
            let mut session = Expander::new(
                &mut self.sources,
                ModuleSources::new(PathBuf::new(), PathBuf::new()),
            )
            .open(ModulePath::new(Self::ENTRY), &[entry])
            .unwrap_or_else(|_| panic!("the entry expands"));
            [first, second].iter().all(|text| {
                let source = session.add_source(Self::ENTRY, text);
                let form = session
                    .read(source)
                    .unwrap_or_else(|_| panic!("the expression reads"))
                    .remove(0);
                session
                    .expression_in(&ModulePath::new(Self::ENTRY), form)
                    .is_ok()
            })
        }
    }

    #[test]
    fn each_expression_spends_from_its_own_step_budget() {
        assert!(ExpressionFixture::new().expands_both(
            ExpressionFixture::LOWERED_LIMIT,
            ExpressionFixture::SPENDS_PAST_THE_LOWERED_LIMIT,
        ));
    }
}