pipa-lang 1.0.0-alpha.1

A tiny template language
Documentation
// SPDX-FileCopyrightText: Copyright 2026 olav@occy.org
// SPDX-License-Identifier: MPL-2.0

//! Count the arguments for custom template functions.

use crate::lang::adapter::Adapter;
use crate::lang::args::should_append_stdin;
use crate::lang::error::Error;
use crate::lang::error::ErrorKind;
use crate::lang::machine::Machine;
use crate::lang::node::Node;
use crate::value::Value;

#[derive(Debug)]
pub struct Arity {
    min: usize,
    max: Option<usize>,
}

impl Arity {
    pub fn new(arity: usize) -> Self {
        Self {
            min: arity,
            max: Some(arity),
        }
    }

    pub fn min(min: usize) -> Self {
        Self { min, max: None }
    }

    pub fn max(max: usize) -> Self {
        Self {
            min: 0,
            max: Some(max),
        }
    }

    pub fn minmax(min: usize, max: usize) -> Self {
        debug_assert!(min > 0, "{min} {max}");
        debug_assert!(max > min, "{min} {max}");

        Self {
            min,
            max: Some(max),
        }
    }
}

pub(crate) fn assert_func_arity(
    adapter: &impl Adapter,
    machine: &Machine<'_>,
    stdin: Option<&Value>,
    node: &Node,
    arity: &Arity,
) -> Result<(), Error> {
    let Arity { min, max } = arity;

    let name = parse_func_name(adapter, machine, node)?;
    let name_count = 1;

    let stdin_count = stdin
        .filter(|_| should_append_stdin(node))
        .map(|_| 1)
        .unwrap_or_default();

    let actual = node
        .list()
        .iter()
        .filter(|p| !matches!(p, Node::Space(_)))
        .count()
        .saturating_sub(name_count)
        .saturating_add(stdin_count);

    if let Some(max) = max {
        if max == min && actual != *max {
            return Err(Error {
                kind: ErrorKind::Func,
                source: adapter.template_source().map(String::from),
                span: machine.span,
                message: format!(
                    "function {name} expects {max} {}: got {}",
                    values_suffix(*max),
                    actual
                ),
            });
        }
    }

    if let Some(max) = max {
        if actual < *min || actual > *max {
            return Err(Error {
                kind: ErrorKind::Func,
                source: adapter.template_source().map(String::from),
                span: machine.span,
                message: format!(
                    "function {name} expects {min}-{max} {}: got {}",
                    values_suffix(*max),
                    actual
                ),
            });
        }
    }

    if actual < arity.min {
        return Err(Error {
            kind: ErrorKind::Func,
            source: adapter.template_source().map(String::from),
            span: machine.span,
            message: format!(
                "function {name} expects at least {min} {}: got {}",
                values_suffix(arity.min),
                actual
            ),
        });
    }

    Ok(())
}

pub(crate) fn parse_func_name<'a>(
    adapter: &impl Adapter,
    machine: &Machine<'_>,
    list: &'a Node,
) -> Result<&'a str, Error> {
    let Some(Node::Label(_, name)) = list.list().first() else {
        return Err(Error {
            kind: ErrorKind::Func,
            source: adapter.template_source().map(String::from),
            span: machine.span,
            message: "missing function name".into(),
        });
    };

    core::str::from_utf8(name.as_bytes()).map_err(|_| Error {
        kind: ErrorKind::Func,
        source: adapter.template_source().map(String::from),
        span: machine.span,
        message: "bad function name encoding".into(),
    })
}

fn values_suffix(n: usize) -> &'static str {
    match n {
        1 => "argument",
        _ => "arguments",
    }
}