knave-expand 0.14.22

The reading, scoping and expansion stages of knave
Documentation
use rustc_hash::FxHashMap;

use crate::core::Keyword;
use crate::reader::Form;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Shape {
    pub header_length: usize,
    pub takes_docstring: bool,
    pub one_per_line: bool,
    pub arms: bool,
    pub bindings: bool,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShapeStatement {
    pub form_name: String,
    pub shape: Shape,
}

#[derive(Clone, Default)]
pub struct ShapeCatalog {
    shapes: FxHashMap<String, Shape>,
}

impl ShapeStatement {
    pub const STATING_FORM: &'static str = "header-shape";
    pub const DOCSTRING_MARKER: &'static str = "docstring";
    pub const ONE_PER_LINE_MARKER: &'static str = "one-per-line";
    pub const ARMS_MARKER: &'static str = "arms";
    pub const BINDINGS_MARKER: &'static str = "bindings";

    pub fn of(form: &Form) -> Option<Self> {
        let [head, stated, markers @ ..] = form.as_list()? else {
            return None;
        };
        (head.as_symbol()?.name() == Self::STATING_FORM).then_some(())?;
        let [form_name, header @ ..] = stated.as_list()? else {
            return None;
        };
        Some(Self {
            form_name: form_name.as_symbol()?.name().to_string(),
            shape: Shape {
                header_length: header.len(),
                takes_docstring: Self::has_marker(markers, Self::DOCSTRING_MARKER),
                one_per_line: Self::has_marker(markers, Self::ONE_PER_LINE_MARKER),
                arms: Self::has_marker(markers, Self::ARMS_MARKER),
                bindings: Self::has_marker(markers, Self::BINDINGS_MARKER),
            },
        })
    }

    fn has_marker(markers: &[Form], marker: &str) -> bool {
        markers.iter().any(|form| {
            form.as_symbol()
                .and_then(|symbol| Keyword::field_name(symbol.name()))
                == Some(marker)
        })
    }
}

impl ShapeCatalog {
    pub fn add(&mut self, statement: &ShapeStatement) {
        self.shapes
            .insert(statement.form_name.clone(), statement.shape);
    }

    pub fn of(&self, form_name: &str) -> Option<Shape> {
        self.shapes.get(form_name).copied()
    }

    pub fn docstring_position(&self, form_name: &str) -> Option<usize> {
        self.of(form_name)
            .filter(|shape| shape.takes_docstring)
            .map(|shape| shape.header_length + 1)
    }
}

#[cfg(test)]
mod tests {
    use super::{Shape, ShapeStatement};
    use crate::reader::{Form, Reader};
    use crate::source::SourceMap;

    struct StatementFixture;

    impl StatementFixture {
        fn statement(text: &str) -> Option<ShapeStatement> {
            ShapeStatement::of(&Self::form(text))
        }

        fn form(text: &str) -> Form {
            let mut sources = SourceMap::default();
            let source = sources.add("shape", text);
            Reader::new(sources.file(source))
                .read()
                .unwrap_or_else(|_| panic!("the statement reads"))
                .remove(0)
        }
    }

    #[test]
    fn a_statement_counts_the_forms_after_the_form_name() {
        let statement = StatementFixture::statement("(header-shape (impl trait type))");
        assert_eq!(
            statement.map(|statement| statement.shape),
            Some(Shape {
                header_length: 2,
                takes_docstring: false,
                one_per_line: false,
                arms: false,
                bindings: false
            })
        );
    }

    #[test]
    fn a_marker_after_the_shape_states_a_docstring() {
        let statement =
            StatementFixture::statement("(header-shape (defn name parameters) :docstring)");
        assert!(statement.is_some_and(|statement| statement.shape.takes_docstring));
    }

    #[test]
    fn a_one_per_line_marker_states_no_docstring() {
        let statement = StatementFixture::statement("(header-shape (enum name) :one-per-line)");
        assert_eq!(
            statement.map(|statement| statement.shape),
            Some(Shape {
                header_length: 1,
                takes_docstring: false,
                one_per_line: true,
                arms: false,
                bindings: false
            })
        );
    }

    #[test]
    fn an_arms_marker_states_arms_alone() {
        let statement = StatementFixture::statement("(header-shape (match value) :arms)");
        assert_eq!(
            statement.map(|statement| statement.shape),
            Some(Shape {
                header_length: 1,
                takes_docstring: false,
                one_per_line: false,
                arms: true,
                bindings: false
            })
        );
    }

    #[test]
    fn a_bindings_marker_states_a_binding_list() {
        let statement = StatementFixture::statement("(header-shape (let bindings) :bindings)");
        assert_eq!(
            statement.map(|statement| statement.shape),
            Some(Shape {
                header_length: 1,
                takes_docstring: false,
                one_per_line: false,
                arms: false,
                bindings: true
            })
        );
    }

    #[test]
    fn a_call_of_another_form_states_no_shape() {
        assert_eq!(StatementFixture::statement("(document (defn name))"), None);
    }

    #[test]
    fn a_shape_that_does_not_start_with_a_name_states_nothing() {
        assert_eq!(
            StatementFixture::statement("(header-shape (\"defn\" name))"),
            None
        );
    }
}