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

use crate::lang::adapter::Adapter;
use crate::lang::arity::parse_func_name;
use crate::lang::error::Error;
use crate::lang::error::ErrorKind;
use crate::lang::eval::eval_spaced;
use crate::lang::machine::Machine;
use crate::lang::node::Node;
use crate::value::Value;
use crate::value::number::Number;
use crate::value::span::Span;

pub(crate) fn eval_args(
    adapter: &impl Adapter,
    machine: &mut Machine<'_>,
    stdin: Option<&Value>,
    mut node: Node,
) -> Result<Vec<Option<Value>>, Error> {
    let Some(Node::Label(..)) = node.try_remove(0) else {
        return Err(Error {
            kind: ErrorKind::Func,
            source: adapter.template_source().map(String::from),
            span: machine.span,
            message: "missing function name".into(),
        });
    };

    if !matches!(node.try_remove(0), None | Some(Node::Space(..))) {
        return Err(Error {
            kind: ErrorKind::Func,
            source: adapter.template_source().map(String::from),
            span: machine.span,
            message: "expected space after function name".into(),
        });
    }

    eval_spaced(adapter, machine, stdin, true, &node)
}

pub(crate) fn eval_unary_arg(
    adapter: &impl Adapter,
    machine: &mut Machine<'_>,
    stdin: Option<&Value>,
    node: Node,
) -> Result<Option<Value>, Error> {
    let len = node.list().len();
    let std = stdin.is_some() && should_append_stdin(&node);

    if len == 0 || len > 3 || len > 1 && std || len == 1 && !std {
        return Err(Error {
            kind: ErrorKind::Func,
            source: adapter.template_source().map(String::from),
            span: machine.span,
            message: "expected exactly 1 argument".into(),
        });
    }

    Ok(eval_args(adapter, machine, stdin, node)?.pop().flatten())
}

pub(crate) fn eval_numeric_args(
    adapter: &impl Adapter,
    machine: &mut Machine<'_>,
    stdin: Option<&Value>,
    node: Node,
) -> Result<Option<Vec<Number>>, Error> {
    Ok(Some(
        eval_args(adapter, machine, stdin, node)?
            .into_iter()
            .map(|v| v.and_then(|v| v.as_number()).unwrap_or_default())
            .collect(),
    ))
}

pub(crate) fn eval_string_args(
    adapter: &impl Adapter,
    machine: &mut Machine<'_>,
    stdin: Option<&Value>,
    node: Node,
) -> Result<Option<Vec<String>>, Error> {
    Ok(eval_args(adapter, machine, stdin, node)?
        .into_iter()
        .map(|v| v.and_then(Value::into_string))
        .collect())
}

pub(crate) fn take_thunk_arg(
    adapter: &impl Adapter,
    machine: &mut Machine<'_>,
    node: &mut Node,
) -> Result<Node, Error> {
    let Some(Node::Space(..)) = node.try_remove(1) else {
        return Err(Error {
            kind: ErrorKind::Func,
            source: adapter.template_source().map(String::from),
            span: machine.span,
            message: format!(
                "missing space after function name: {}",
                parse_func_name(adapter, machine, node)?
            ),
        });
    };

    let Some(arg) = node.try_remove(1) else {
        return Err(Error {
            kind: ErrorKind::Func,
            source: adapter.template_source().map(String::from),
            span: machine.span,
            message: format!(
                "expects an argument: {}",
                parse_func_name(adapter, machine, node)?
            ),
        });
    };

    if !matches!(arg, Node::Thunk(..)) {
        return Err(Error {
            kind: ErrorKind::Func,
            source: adapter.template_source().map(String::from),
            span: machine.span,
            message: format!(
                "expects a function as first argument: {}",
                parse_func_name(adapter, machine, node)?
            ),
        });
    }

    into_thunk_arg(adapter, machine, node, arg)
}

pub(crate) fn into_thunk_arg(
    adapter: &impl Adapter,
    machine: &Machine<'_>,
    node: &Node,
    arg: Node,
) -> Result<Node, Error> {
    match arg {
        Node::Thunk(..) => Ok(arg),
        Node::Bytes(..) | Node::Label(..) | Node::List(..) | Node::Map(..) | Node::Paren(..) => {
            Ok(Node::Thunk(Span::from(&arg), vec![arg]))
        }
        _ => Err(Error {
            kind: ErrorKind::Func,
            source: adapter.template_source().map(String::from),
            span: machine.span,
            message: format!(
                "function expects an argument: {}",
                parse_func_name(adapter, machine, node)?
            ),
        }),
    }
}

pub(crate) fn thunk_as_pipe(
    adapter: &impl Adapter,
    machine: &Machine<'_>,
    node: Node,
) -> Result<Node, Error> {
    let span = Span::from(&node);

    if let Node::Thunk(..) = node {
        if let Some(list) = node.into_list() {
            return Ok(Node::Pipe(span, list));
        }
    }

    Err(Error {
        kind: ErrorKind::Func,
        source: adapter.template_source().map(String::from),
        span: machine.span,
        message: "expected thunk for pipe".into(),
    })
}

pub(crate) fn should_append_stdin(list: &Node) -> bool {
    matches!(list, Node::Pipe(..) | Node::Thunk(..)) && !has_placeholders(list)
}

fn has_placeholders(list: &Node) -> bool {
    list.list().iter().any(|n| match n {
        Node::Label(_, n) if n.as_bytes() == b"_" => true,
        Node::Label(_, n) if n.as_bytes().starts_with(b"_.") => true,
        Node::Label(_, n) if n.as_bytes().starts_with(b"_[") => true,
        Node::Label(..) => false,
        Node::Tag(..) => false,
        Node::Bytes(..) => false,
        Node::Space(..) => false,
        Node::Comma(..) => false,
        Node::Thunk(..) => false,
        Node::And(..) => has_placeholders(n),
        Node::List(..) => has_placeholders(n),
        Node::Map(..) => has_placeholders(n),
        Node::Or(..) => has_placeholders(n),
        Node::Pipe(..) => has_placeholders(n),
        Node::Paren(..) => has_placeholders(n),
        Node::Bracket(..) => has_placeholders(n),
        Node::Semicolon(..) => has_placeholders(n),
    })
}

pub(crate) fn arg_into_string(value: Option<Value>) -> Option<String> {
    value.map(|v| String::from_utf8_lossy(&v.join()).into_owned())
}