Skip to main content

harn_parser/builtin_signatures/
types.rs

1//! Single, rich descriptor for every builtin known to Harn.
2//!
3//! Both the static type checker (this crate) and the runtime VM (`harn-vm`)
4//! consume these signatures: arity, per-parameter types, generic bindings,
5//! `where`-clause bounds and return types all live here. The shape is
6//! deliberately `const`-constructible (everything is `&'static [...]` and
7//! `Copy`) so each entry in `signatures/*.rs` is a single static literal.
8//!
9//! Generic builtins are expressed by naming their type parameters in
10//! `type_params` and using
11//! [`Ty::Generic`] / [`Ty::Apply`] / [`Ty::SchemaOf`] in the param/return
12//! positions.
13
14use crate::ast::{ShapeField, TypeExpr};
15
16/// A complete, static description of one builtin: identifier, arity range,
17/// per-parameter types, generic type parameters, return type, and any
18/// where-clause bounds the type checker should enforce on call.
19#[derive(Debug, Clone, Copy)]
20pub struct BuiltinSignature {
21    /// Builtin name as registered in the VM and referenced from Harn source.
22    pub name: &'static str,
23    /// Positional parameters in declaration order. Trailing entries with
24    /// `optional: true` define the lower bound of the arity range; the
25    /// remaining entries plus `has_rest` define the upper bound.
26    pub params: &'static [Param],
27    /// Statically-known return type. Use [`Ty::Any`] when the return is
28    /// genuinely dynamic (e.g. `json_parse`).
29    pub returns: Ty,
30    /// Generic type parameter names declared on this builtin (e.g. `["T"]`
31    /// for `schema_parse<T>`).
32    pub type_params: &'static [&'static str],
33    /// True when the final parameter is variadic (rest). When set, the
34    /// effective arity upper bound is unbounded and the runtime will treat
35    /// trailing args as the rest-list.
36    pub has_rest: bool,
37    /// `where T: Foo` constraints. Each entry binds a generic type
38    /// parameter name to the name of an interface it must implement.
39    pub where_clauses: &'static [(&'static str, &'static str)],
40}
41
42/// One parameter slot inside a [`BuiltinSignature`].
43#[derive(Debug, Clone, Copy)]
44pub struct Param {
45    pub name: &'static str,
46    pub ty: Ty,
47    /// True when this parameter has a default at the call site (so it may
48    /// be omitted). All optional params must be trailing.
49    pub optional: bool,
50}
51
52impl Param {
53    pub const fn new(name: &'static str, ty: Ty) -> Self {
54        Self {
55            name,
56            ty,
57            optional: false,
58        }
59    }
60
61    pub const fn optional(name: &'static str, ty: Ty) -> Self {
62        Self {
63            name,
64            ty,
65            optional: true,
66        }
67    }
68}
69
70/// `const`-friendly type IR used in builtin descriptors. Mirrors the runtime
71/// [`TypeExpr`] but is constructable in `const` position with no allocation.
72/// Convert to `TypeExpr` at the boundary via [`Ty::to_type_expr`].
73#[derive(Debug, Clone, Copy)]
74pub enum Ty {
75    /// A primitive or user-defined named type: `int`, `string`, `bool`,
76    /// `float`, `nil`, `bytes`, `dict`, `list`, `closure`, `duration`,
77    /// `any`, etc.
78    Named(&'static str),
79    /// Reference to a generic type parameter declared on the enclosing
80    /// signature (e.g. `Generic("T")`).
81    Generic(&'static str),
82    /// Untyped/dynamic. Skips type validation at runtime; the static
83    /// checker treats it as compatible with everything.
84    Any,
85    /// Optional sugar for `T | nil`.
86    Optional(&'static Ty),
87    /// Generic application: `List<T>` is `Apply("list", &[T])`,
88    /// `Result<T, E>` is `Apply("Result", &[T, E])`, `Schema<T>` is
89    /// [`Ty::SchemaOf`].
90    Apply(&'static str, &'static [Ty]),
91    /// Union of N alternatives. Empty unions are rejected by the
92    /// [`Ty::to_type_expr`] converter.
93    Union(&'static [Ty]),
94    /// Function type. Stores params and return as references so the literal
95    /// stays `Copy`.
96    Fn(&'static [Ty], &'static Ty),
97    /// Record/shape type with named fields.
98    Shape(&'static [ShapeFieldDescriptor]),
99    /// `Schema<T>` marker — semantically `Apply("Schema", &[Generic(T)])`
100    /// but distinguished so the type checker can pull the bound `T` from
101    /// the *value* of the schema arg (not its declared type).
102    SchemaOf(&'static str),
103    /// Bottom type (no return).
104    Never,
105    /// Integer literal type: `0`, `1`. Assignable to `int`.
106    LitInt(i64),
107    /// String literal type: `"pass"`. Assignable to `string`.
108    LitString(&'static str),
109}
110
111#[derive(Debug, Clone, Copy)]
112pub struct ShapeFieldDescriptor {
113    pub name: &'static str,
114    pub ty: Ty,
115    pub optional: bool,
116}
117
118impl ShapeFieldDescriptor {
119    pub const fn new(name: &'static str, ty: Ty) -> Self {
120        Self {
121            name,
122            ty,
123            optional: false,
124        }
125    }
126
127    pub const fn optional(name: &'static str, ty: Ty) -> Self {
128        Self {
129            name,
130            ty,
131            optional: true,
132        }
133    }
134}
135
136impl Ty {
137    /// Materialize as a runtime [`TypeExpr`]. Generic references stay as
138    /// `Named(name)` so the checker's existing scope-based generic-param
139    /// resolution applies.
140    pub fn to_type_expr(&self) -> TypeExpr {
141        match self {
142            Ty::Named(name) => TypeExpr::Named((*name).into()),
143            Ty::Generic(name) => TypeExpr::Named((*name).into()),
144            Ty::Any => TypeExpr::Named("any".into()),
145            Ty::Optional(inner) => {
146                TypeExpr::Union(vec![inner.to_type_expr(), TypeExpr::Named("nil".into())])
147            }
148            Ty::Apply(name, args) => TypeExpr::Applied {
149                name: (*name).into(),
150                args: args.iter().map(Ty::to_type_expr).collect(),
151            },
152            Ty::Union(members) => TypeExpr::Union(members.iter().map(Ty::to_type_expr).collect()),
153            Ty::Fn(params, return_type) => TypeExpr::FnType {
154                params: params.iter().map(Ty::to_type_expr).collect(),
155                return_type: Box::new(return_type.to_type_expr()),
156            },
157            Ty::Shape(fields) => TypeExpr::Shape(
158                fields
159                    .iter()
160                    .map(|f| ShapeField {
161                        name: f.name.into(),
162                        type_expr: f.ty.to_type_expr(),
163                        optional: f.optional,
164                    })
165                    .collect(),
166            ),
167            Ty::SchemaOf(name) => TypeExpr::Applied {
168                name: "Schema".into(),
169                args: vec![TypeExpr::Named((*name).into())],
170            },
171            Ty::Never => TypeExpr::Never,
172            Ty::LitInt(v) => TypeExpr::LitInt(*v),
173            Ty::LitString(s) => TypeExpr::LitString((*s).into()),
174        }
175    }
176
177    /// True when this type carries no constraints (validation is a no-op).
178    pub fn is_any(&self) -> bool {
179        matches!(self, Ty::Any)
180    }
181}
182
183impl BuiltinSignature {
184    /// Non-generic, fixed-arity builtin: no type parameters, no rest, no
185    /// where-clause bounds. Covers ~70% of the registry; lets each call
186    /// site stay on a single logical line.
187    pub const fn simple(name: &'static str, params: &'static [Param], returns: Ty) -> Self {
188        Self {
189            name,
190            params,
191            returns,
192            type_params: &[],
193            has_rest: false,
194            where_clauses: &[],
195        }
196    }
197
198    /// Non-generic builtin whose final parameter is variadic (rest).
199    /// Equivalent to [`Self::simple`] with `has_rest: true`.
200    pub const fn variadic(name: &'static str, params: &'static [Param], returns: Ty) -> Self {
201        Self {
202            name,
203            params,
204            returns,
205            type_params: &[],
206            has_rest: true,
207            where_clauses: &[],
208        }
209    }
210
211    /// Generic, fixed-arity builtin: declares type parameters, no rest,
212    /// no where-clause bounds. Use the struct literal directly when both
213    /// generics and where-clauses or rest are needed.
214    pub const fn generic(
215        name: &'static str,
216        type_params: &'static [&'static str],
217        params: &'static [Param],
218        returns: Ty,
219    ) -> Self {
220        Self {
221            name,
222            params,
223            returns,
224            type_params,
225            has_rest: false,
226            where_clauses: &[],
227        }
228    }
229
230    /// Number of required parameters (those without defaults).
231    pub fn required_params(&self) -> usize {
232        self.params.iter().filter(|p| !p.optional).count()
233    }
234
235    /// True when this builtin recognises `name` as one of its declared
236    /// generic type parameters.
237    pub fn is_type_param(&self, name: &str) -> bool {
238        self.type_params.contains(&name)
239    }
240
241    /// True when this builtin declares any generic type parameters.
242    pub fn is_generic(&self) -> bool {
243        !self.type_params.is_empty()
244    }
245
246    /// Materialize the type parameter names as owned strings (for use in
247    /// the type checker's existing scope/binding APIs which key off
248    /// `Vec<String>`).
249    pub fn type_param_names(&self) -> Vec<String> {
250        self.type_params.iter().map(|s| (*s).to_string()).collect()
251    }
252
253    /// Materialize per-parameter types as owned [`TypeExpr`]s for the
254    /// type checker's call-site validation. `Ty::Any` becomes
255    /// `Named("any")`; generic params become `Named(T)` so the existing
256    /// generic-binding logic resolves them through scope.
257    pub fn param_type_exprs(&self) -> Vec<TypeExpr> {
258        self.params.iter().map(|p| p.ty.to_type_expr()).collect()
259    }
260
261    /// Owned [`TypeExpr`] return type. Use [`Ty::is_any`] on
262    /// [`BuiltinSignature::returns`] first if you want to distinguish
263    /// "returns any" from "no static return info".
264    pub fn return_type_expr(&self) -> TypeExpr {
265        self.returns.to_type_expr()
266    }
267
268    /// Where-clause constraints as `(type_param, interface)` strings.
269    pub fn where_clause_strings(&self) -> Vec<(String, String)> {
270        self.where_clauses
271            .iter()
272            .map(|(tp, iface)| ((*tp).to_string(), (*iface).to_string()))
273            .collect()
274    }
275}
276
277/// Public view of one builtin used by `harn-lint` and other crates that need
278/// just identifier + return-type hints (no parameter types).
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub struct BuiltinMetadata {
281    pub name: &'static str,
282    pub return_types: &'static [&'static str],
283}
284
285// ---- Convenience constants ----
286//
287// Used pervasively in `signatures/*.rs` to keep individual entries terse.
288// Add new constants here when a type appears repeatedly enough to warrant
289// a shorthand (avoid one-off shorthands).
290
291pub const TY_ANY: Ty = Ty::Any;
292pub const TY_BOOL: Ty = Ty::Named("bool");
293pub const TY_BYTES: Ty = Ty::Named("bytes");
294pub const TY_CLOSURE: Ty = Ty::Named("closure");
295pub const TY_DICT: Ty = Ty::Named("dict");
296pub const TY_DURATION: Ty = Ty::Named("duration");
297pub const TY_FLOAT: Ty = Ty::Named("float");
298pub const TY_INT: Ty = Ty::Named("int");
299pub const TY_LIST: Ty = Ty::Named("list");
300pub const TY_NEVER: Ty = Ty::Never;
301pub const TY_NIL: Ty = Ty::Named("nil");
302pub const TY_STRING: Ty = Ty::Named("string");
303
304/// `string | nil`.
305pub const TY_STRING_OR_NIL: Ty = Ty::Union(&[TY_STRING, TY_NIL]);
306/// `int | nil`.
307pub const TY_INT_OR_NIL: Ty = Ty::Union(&[TY_INT, TY_NIL]);
308/// `dict | nil`.
309pub const TY_DICT_OR_NIL: Ty = Ty::Union(&[TY_DICT, TY_NIL]);
310/// `bytes | nil`.
311pub const TY_BYTES_OR_NIL: Ty = Ty::Union(&[TY_BYTES, TY_NIL]);
312/// `int | float`.
313pub const TY_NUMBER: Ty = Ty::Union(&[TY_INT, TY_FLOAT]);