vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Template loader + interpolator.
//!
//! All templates are plain text files loaded at compile time via `include_str!`.
//! The renderer implements a Mustache-subset with no external dependencies:
//!
//! - `{field}` — scalar interpolation
//! - `{list:name}...{/list}` — loop over a list field
//! - `{if:cond}...{/if}` — conditional block
//! - `{include:name}` — include another template loaded at compile time

use std::collections::HashMap;
use std::fmt;

/// Error returned when template rendering fails.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateError {
    /// A scalar field was referenced but not found in the context.
    UnknownField(String),
    /// A template directive was malformed (unclosed brace, missing end tag, etc.).
    MalformedDirective,
    /// An `{include:name}` directive referenced a template that does not exist.
    IncludeNotFound(String),
}

impl fmt::Display for TemplateError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::UnknownField(name) => write!(f, "unknown template field: {name}"),
            Self::MalformedDirective => write!(f, "malformed template directive"),
            Self::IncludeNotFound(name) => write!(f, "included template not found: {name}"),
        }
    }
}

impl std::error::Error for TemplateError {}

/// Thin wrapper over template data: scalar fields, lists, and conditionals.
#[derive(Debug, Default, Clone)]
pub struct TemplateContext {
    /// Scalar string values keyed by field name.
    pub scalars: HashMap<&'static str, String>,
    /// List values keyed by field name; each element is a nested context.
    pub lists: HashMap<&'static str, Vec<TemplateContext>>,
    /// Boolean conditionals keyed by name.
    pub conditionals: HashMap<&'static str, bool>,
}

impl TemplateContext {
    /// Create an empty context.
    #[inline]
    pub fn new() -> Self {
        Self::default()
    }

    /// Insert a scalar field.
    #[inline]
    pub fn with_scalar(mut self, key: &'static str, value: impl Into<String>) -> Self {
        self.scalars.insert(key, value.into());
        self
    }

    /// Insert a list field.
    #[inline]
    pub fn with_list(mut self, key: &'static str, value: Vec<TemplateContext>) -> Self {
        self.lists.insert(key, value);
        self
    }

    /// Insert a conditional flag.
    #[inline]
    pub fn with_conditional(mut self, key: &'static str, value: bool) -> Self {
        self.conditionals.insert(key, value);
        self
    }
}

/// Render `template_src` using `context`, expanding Mustache-subset directives.
///
/// Supported directives:
/// - `{field}` — scalar interpolation
/// - `{list:name}...{/list}` — loop over a list field
/// - `{if:cond}...{/if}` — conditional block
/// - `{include:name}` — include another template loaded at compile time
#[inline]
pub fn render(template_src: &str, context: &TemplateContext) -> Result<String, TemplateError> {
    let mut out = String::with_capacity(template_src.len() * 2);
    render_inner(template_src, context, &mut out)?;
    Ok(out)
}

/// Return the executable Rust-test template for a generated test type.
///
/// # Errors
///
/// Returns [`TemplateError::IncludeNotFound`] when the generator asks for a
/// test template that is not present in `generate/templates/*.mustache`.
#[inline]
pub fn rust_test_template(name: &str) -> Result<&'static str, TemplateError> {
    match name {
        "law" => Ok(include_str!("law.mustache")),
        "spec_table" => Ok(include_str!("spec_table.mustache")),
        "backend_equiv" => Ok(include_str!("backend_equiv.mustache")),
        "point_parity" => Ok(include_str!("point_parity.mustache")),
        "validation" => Ok(include_str!("validation.mustache")),
        "mutation_kill" => Ok(include_str!("mutation_kill.mustache")),
        _ => Err(TemplateError::IncludeNotFound(name.to_string())),
    }
}

fn resolve_include(name: &str) -> Option<&'static str> {
    match name {
        "preamble" => Some(include_str!("preamble.tmpl")),
        "op_correctness" => Some(include_str!("op_correctness.tmpl")),
        "law" => Some(include_str!("law.tmpl")),
        "validation" => Some(include_str!("validation.tmpl")),
        "backend_equiv" => Some(include_str!("backend_equiv.tmpl")),
        "archetype" => Some(include_str!("archetype.tmpl")),
        "mutation_kill" => Some(include_str!("mutation_kill.tmpl")),
        _ => None,
    }
}

fn find_block_end(src: &str, open_prefix: &str, close_tag: &str) -> Result<usize, TemplateError> {
    let mut depth = 1;
    let mut i = 0;
    while i < src.len() {
        if src[i..].starts_with(open_prefix) {
            depth += 1;
            i += open_prefix.len();
        } else if src[i..].starts_with(close_tag) {
            depth -= 1;
            if depth == 0 {
                return Ok(i);
            }
            i += close_tag.len();
        } else {
            i += 1;
        }
    }
    Err(TemplateError::MalformedDirective)
}

fn render_inner(src: &str, ctx: &TemplateContext, out: &mut String) -> Result<(), TemplateError> {
    let mut i = 0;
    while i < src.len() {
        match src[i..].find('{') {
            Some(pos) => {
                out.push_str(&src[i..i + pos]);
                i += pos;

                let end = src[i..]
                    .find('}')
                    .ok_or(TemplateError::MalformedDirective)?;
                let directive = &src[i + 1..i + end];
                i += end + 1;

                if let Some(name) = directive.strip_prefix("list:") {
                    let body_end = find_block_end(&src[i..], "{list:", "{/list}")?;
                    let body = &src[i..i + body_end];
                    i += body_end + "{/list}".len();

                    let items = ctx
                        .lists
                        .get(name)
                        .ok_or_else(|| TemplateError::UnknownField(name.to_string()))?;
                    for item in items {
                        render_inner(body, item, out)?;
                    }
                } else if let Some(name) = directive.strip_prefix("if:") {
                    let body_end = find_block_end(&src[i..], "{if:", "{/if}")?;
                    let body = &src[i..i + body_end];
                    i += body_end + "{/if}".len();

                    if ctx.conditionals.get(name).copied().unwrap_or(false) {
                        render_inner(body, ctx, out)?;
                    }
                } else if let Some(name) = directive.strip_prefix("include:") {
                    let included = resolve_include(name)
                        .ok_or_else(|| TemplateError::IncludeNotFound(name.to_string()))?;
                    render_inner(included, ctx, out)?;
                } else {
                    let val = ctx
                        .scalars
                        .get(directive)
                        .ok_or_else(|| TemplateError::UnknownField(directive.to_string()))?;
                    out.push_str(val);
                }
            }
            None => {
                out.push_str(&src[i..]);
                break;
            }
        }
    }
    Ok(())
}