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    /// Number of required parameters (those without defaults).
185    pub fn required_params(&self) -> usize {
186        self.params.iter().filter(|p| !p.optional).count()
187    }
188
189    /// True when this builtin recognises `name` as one of its declared
190    /// generic type parameters.
191    pub fn is_type_param(&self, name: &str) -> bool {
192        self.type_params.contains(&name)
193    }
194
195    /// True when this builtin declares any generic type parameters.
196    pub fn is_generic(&self) -> bool {
197        !self.type_params.is_empty()
198    }
199
200    /// Materialize the type parameter names as owned strings (for use in
201    /// the type checker's existing scope/binding APIs which key off
202    /// `Vec<String>`).
203    pub fn type_param_names(&self) -> Vec<String> {
204        self.type_params.iter().map(|s| (*s).to_string()).collect()
205    }
206
207    /// Materialize per-parameter types as owned [`TypeExpr`]s for the
208    /// type checker's call-site validation. `Ty::Any` becomes
209    /// `Named("any")`; generic params become `Named(T)` so the existing
210    /// generic-binding logic resolves them through scope.
211    pub fn param_type_exprs(&self) -> Vec<TypeExpr> {
212        self.params.iter().map(|p| p.ty.to_type_expr()).collect()
213    }
214
215    /// Owned [`TypeExpr`] return type. Use [`Ty::is_any`] on
216    /// [`BuiltinSignature::returns`] first if you want to distinguish
217    /// "returns any" from "no static return info".
218    pub fn return_type_expr(&self) -> TypeExpr {
219        self.returns.to_type_expr()
220    }
221
222    /// Where-clause constraints as `(type_param, interface)` strings.
223    pub fn where_clause_strings(&self) -> Vec<(String, String)> {
224        self.where_clauses
225            .iter()
226            .map(|(tp, iface)| ((*tp).to_string(), (*iface).to_string()))
227            .collect()
228    }
229}
230
231/// Public view of one builtin used by `harn-lint` and other crates that need
232/// just identifier + return-type hints (no parameter types).
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct BuiltinMetadata {
235    pub name: &'static str,
236    pub return_types: &'static [&'static str],
237}
238
239// ---- Convenience constants ----
240//
241// Used pervasively in `signatures/*.rs` to keep individual entries terse.
242// Add new constants here when a type appears repeatedly enough to warrant
243// a shorthand (avoid one-off shorthands).
244
245pub const TY_ANY: Ty = Ty::Any;
246pub const TY_BOOL: Ty = Ty::Named("bool");
247pub const TY_BYTES: Ty = Ty::Named("bytes");
248pub const TY_CLOSURE: Ty = Ty::Named("closure");
249pub const TY_DICT: Ty = Ty::Named("dict");
250pub const TY_DURATION: Ty = Ty::Named("duration");
251pub const TY_FLOAT: Ty = Ty::Named("float");
252pub const TY_INT: Ty = Ty::Named("int");
253pub const TY_LIST: Ty = Ty::Named("list");
254pub const TY_NEVER: Ty = Ty::Never;
255pub const TY_NIL: Ty = Ty::Named("nil");
256pub const TY_STRING: Ty = Ty::Named("string");
257
258/// `string | nil`.
259pub const TY_STRING_OR_NIL: Ty = Ty::Union(&[TY_STRING, TY_NIL]);
260/// `int | nil`.
261pub const TY_INT_OR_NIL: Ty = Ty::Union(&[TY_INT, TY_NIL]);
262/// `dict | nil`.
263pub const TY_DICT_OR_NIL: Ty = Ty::Union(&[TY_DICT, TY_NIL]);
264/// `bytes | nil`.
265pub const TY_BYTES_OR_NIL: Ty = Ty::Union(&[TY_BYTES, TY_NIL]);
266/// `int | float`.
267pub const TY_NUMBER: Ty = Ty::Union(&[TY_INT, TY_FLOAT]);