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}