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]);