Skip to main content

pine_interpreter/
signature.rs

1//! What a builtin accepts, so a wrong argument can be reported before the
2//! script runs.
3//!
4//! [`ParamType`] mirrors the field type a builtin declares — `f64`, `String`,
5//! `bool` and so on — not Pine's own type system. A parameter therefore rejects
6//! exactly what the runtime's conversion would reject, which is what makes the
7//! check safe: it can only turn a guaranteed runtime error into a compile-time
8//! one, never reject a call that would have worked.
9
10use pine_ast::Literal;
11
12/// The kind of value a parameter accepts, taken from the builtin's field type.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ParamType {
15    /// `f64`, `i64` or `Num`.
16    Number,
17    String,
18    Bool,
19    Color,
20    /// `Value<O>` — the builtin inspects the value itself, so anything goes.
21    Any,
22}
23
24impl ParamType {
25    /// Whether a literal argument can be passed here, mirroring the conversion
26    /// the builtin will apply at runtime.
27    pub fn accepts(self, literal: &Literal) -> bool {
28        match self {
29            ParamType::Any => true,
30            // `na` stands in for any type.
31            _ if matches!(literal, Literal::Na) => true,
32            // The numeric conversion takes numbers and bools; a string is a
33            // type error.
34            ParamType::Number | ParamType::Bool => {
35                !matches!(literal, Literal::String(_) | Literal::HexColor(_))
36            }
37            // Any scalar renders as a string.
38            ParamType::String => true,
39            ParamType::Color => matches!(literal, Literal::HexColor(_) | Literal::String(_)),
40        }
41    }
42
43    pub fn describe(self) -> &'static str {
44        match self {
45            ParamType::Number => "a number",
46            ParamType::String => "a string",
47            ParamType::Bool => "a bool",
48            ParamType::Color => "a color",
49            ParamType::Any => "a value",
50        }
51    }
52}
53
54/// One parameter of a builtin.
55#[derive(Debug, Clone)]
56pub struct Param {
57    pub name: String,
58    pub ty: ParamType,
59    /// False when the parameter has a default and may be omitted.
60    pub required: bool,
61    /// True for a trailing parameter that soaks up any number of arguments.
62    pub variadic: bool,
63    /// True when the argument is passed unevaluated (as a captured `Expr`),
64    /// for intrinsics like `request.security` that run it in another context.
65    pub lazy: bool,
66}
67
68/// The parameters a builtin accepts, in positional order.
69#[derive(Debug, Clone, Default)]
70pub struct BuiltinSignature {
71    pub params: Vec<Param>,
72}
73
74impl BuiltinSignature {
75    /// The parameter an argument at `index` binds to; a trailing variadic
76    /// parameter takes every argument past it. `None` means the call passed
77    /// more arguments than the builtin accepts.
78    pub fn positional(&self, index: usize) -> Option<&Param> {
79        match self.params.get(index) {
80            Some(param) => Some(param),
81            None => self.params.last().filter(|last| last.variadic),
82        }
83    }
84
85    pub fn named(&self, name: &str) -> Option<&Param> {
86        self.params.iter().find(|param| param.name == name)
87    }
88
89    /// Whether the positional argument at `index` is passed unevaluated.
90    pub fn positional_is_lazy(&self, index: usize) -> bool {
91        self.positional(index).is_some_and(|param| param.lazy)
92    }
93
94    /// Whether the named argument `name` is passed unevaluated.
95    pub fn named_is_lazy(&self, name: &str) -> bool {
96        self.named(name).is_some_and(|param| param.lazy)
97    }
98
99    /// The most positional arguments accepted, or `None` when variadic.
100    pub fn max_positional(&self) -> Option<usize> {
101        if self.params.last().is_some_and(|last| last.variadic) {
102            None
103        } else {
104            Some(self.params.len())
105        }
106    }
107}