knave-expand 0.9.3

The reading, scoping and expansion stages of knave
Documentation
use crate::core::KernelForm;
use crate::diagnostic::{Diagnostic, DiagnosticCode, Label};
use crate::reader::{Form, FormKind, Symbol};

pub struct Shape<'a> {
    kernel: KernelForm,
    form: &'a Form,
}

impl<'a> Shape<'a> {
    pub fn new(kernel: KernelForm, form: &'a Form) -> Self {
        Self { kernel, form }
    }

    pub fn items(&self) -> &'a [Form] {
        self.form.as_list().unwrap_or_default()
    }

    pub fn exactly(&self, count: usize) -> Result<&'a [Form], Diagnostic> {
        let items = self.items();
        if items.len() == count {
            Ok(items)
        } else {
            Err(self.malformed(self.form))
        }
    }

    pub fn at_least(&self, count: usize) -> Result<&'a [Form], Diagnostic> {
        let items = self.items();
        if items.len() >= count {
            Ok(items)
        } else {
            Err(self.malformed(self.form))
        }
    }

    pub fn symbol(&self, part: &'a Form) -> Result<&'a Symbol, Diagnostic> {
        part.as_symbol().ok_or_else(|| self.malformed(part))
    }

    pub fn list(&self, part: &'a Form) -> Result<&'a [Form], Diagnostic> {
        part.as_list().ok_or_else(|| self.malformed(part))
    }

    pub fn string(&self, part: &'a Form) -> Result<&'a str, Diagnostic> {
        match part.kind() {
            FormKind::String(text) => Ok(text),
            _ => Err(self.malformed(part)),
        }
    }

    pub fn malformed(&self, part: &Form) -> Diagnostic {
        self.malformed_with_help(part, format!("write it as `{}`", self.kernel.shape()))
    }

    pub fn malformed_form_with_help(&self, help: impl Into<String>) -> Diagnostic {
        self.malformed_with_help(self.form, help)
    }

    pub fn malformed_with_help(&self, part: &Form, help: impl Into<String>) -> Diagnostic {
        let name = self.kernel.name();
        Diagnostic::error(
            DiagnosticCode::MALFORMED_FORM,
            format!("malformed `{name}`"),
            Label::new(
                part.span(),
                format!("this does not fit the shape of `{name}`"),
            ),
        )
        .with_help(help)
    }
}