Skip to main content

harn_builtin_meta/
lib.rs

1//! Const-constructible type definitions for Harn builtin signatures.
2//!
3//! Both `harn-parser` (for typechecking) and `harn-vm` (for runtime metadata)
4//! consume these shapes. Living in a dep-free crate lets the parser see the
5//! types without depending on the VM, and lets the `#[harn_builtin]` proc-macro
6//! emit `const` literals that link into either side.
7//!
8//! `Ty::to_type_expr` and friends, which convert into the parser's runtime
9//! `TypeExpr`, live in `harn-parser` since they depend on parser-internal AST.
10//!
11//! The [`shapes`] submodule holds the named structural-record consts
12//! (`LLM_CALL_OPTIONS`, `LLM_CALL_RESULT`, `TRANSCRIPT`, …) shared by the
13//! parser's static typechecking tables and the `#[harn_builtin]` macro's
14//! `@NAME` signature injection.
15
16pub mod llm_options;
17pub mod runtime_type_tags;
18pub mod shapes;
19pub mod signatures;
20
21/// A complete, static description of one builtin: identifier, arity range,
22/// per-parameter types, generic type parameters, return type, and any
23/// where-clause bounds the type checker should enforce on call.
24#[derive(Debug, Clone, Copy)]
25pub struct BuiltinSignature {
26    /// Builtin name as registered in the VM and referenced from Harn source.
27    pub name: &'static str,
28    /// Positional parameters in declaration order. Trailing entries with
29    /// `optional: true` define the lower bound of the arity range; the
30    /// remaining entries plus `has_rest` define the upper bound.
31    pub params: &'static [Param],
32    /// Statically-known return type. Use [`Ty::Any`] when the return is
33    /// genuinely dynamic (e.g. `json_parse`).
34    pub returns: Ty,
35    /// Generic type parameter names declared on this builtin (e.g. `["T"]`
36    /// for `schema_parse<T>`).
37    pub type_params: &'static [&'static str],
38    /// True when the final parameter is variadic (rest). When set, the
39    /// effective arity upper bound is unbounded and the runtime will treat
40    /// trailing args as the rest-list.
41    pub has_rest: bool,
42    /// `where T: Foo` constraints. Each entry binds a generic type
43    /// parameter name to the name of an interface it must implement.
44    pub where_clauses: &'static [(&'static str, &'static str)],
45}
46
47/// One parameter slot inside a [`BuiltinSignature`].
48#[derive(Debug, Clone, Copy)]
49pub struct Param {
50    pub name: &'static str,
51    pub ty: Ty,
52    /// True when this parameter has a default at the call site (so it may
53    /// be omitted). All optional params must be trailing.
54    pub optional: bool,
55}
56
57impl Param {
58    pub const fn new(name: &'static str, ty: Ty) -> Self {
59        Self {
60            name,
61            ty,
62            optional: false,
63        }
64    }
65
66    pub const fn optional(name: &'static str, ty: Ty) -> Self {
67        Self {
68            name,
69            ty,
70            optional: true,
71        }
72    }
73}
74
75/// `const`-friendly type IR used in builtin descriptors. Mirrors the runtime
76/// `TypeExpr` from `harn-parser` but is constructable in `const` position with
77/// no allocation. Convert to `TypeExpr` at the boundary via the parser-side
78/// `Ty::to_type_expr` helper.
79#[derive(Debug, Clone, Copy)]
80pub enum Ty {
81    /// A primitive or user-defined named type: `int`, `string`, `bool`,
82    /// `float`, `nil`, `bytes`, `dict`, `list`, `closure`, `duration`,
83    /// `any`, etc.
84    Named(&'static str),
85    /// Reference to a generic type parameter declared on the enclosing
86    /// signature (e.g. `Generic("T")`).
87    Generic(&'static str),
88    /// Untyped/dynamic. Skips type validation at runtime; the static
89    /// checker treats it as compatible with everything.
90    Any,
91    /// Optional sugar for `T | nil`.
92    Optional(&'static Ty),
93    /// Generic application: `List<T>` is `Apply("list", &[T])`,
94    /// `Result<T, E>` is `Apply("Result", &[T, E])`, `Schema<T>` is
95    /// [`Ty::SchemaOf`].
96    Apply(&'static str, &'static [Ty]),
97    /// Union of N alternatives. Empty unions are rejected by the
98    /// parser-side converter.
99    Union(&'static [Ty]),
100    /// Function type. Stores params and return as references so the literal
101    /// stays `Copy`.
102    Fn(&'static [Ty], &'static Ty),
103    /// Record/shape type with named fields.
104    Shape(&'static [ShapeFieldDescriptor]),
105    /// `Schema<T>` marker — semantically `Apply("Schema", &[Generic(T)])`
106    /// but distinguished so the type checker can pull the bound `T` from
107    /// the *value* of the schema arg (not its declared type).
108    SchemaOf(&'static str),
109    /// Bottom type (no return).
110    Never,
111    /// Integer literal type: `0`, `1`. Assignable to `int`.
112    LitInt(i64),
113    /// String literal type: `"pass"`. Assignable to `string`.
114    LitString(&'static str),
115}
116
117#[derive(Debug, Clone, Copy)]
118pub struct ShapeFieldDescriptor {
119    pub name: &'static str,
120    pub ty: Ty,
121    pub optional: bool,
122}
123
124impl ShapeFieldDescriptor {
125    pub const fn new(name: &'static str, ty: Ty) -> Self {
126        Self {
127            name,
128            ty,
129            optional: false,
130        }
131    }
132
133    pub const fn optional(name: &'static str, ty: Ty) -> Self {
134        Self {
135            name,
136            ty,
137            optional: true,
138        }
139    }
140}
141
142impl Ty {
143    /// True when this type carries no constraints (validation is a no-op).
144    pub const fn is_any(&self) -> bool {
145        matches!(self, Ty::Any)
146    }
147}
148
149impl core::fmt::Display for Ty {
150    /// Render a parsed [`Ty`] back into the `#[harn_builtin]` sig grammar.
151    /// Round-trip target: parsing the output through the proc-macro's
152    /// sig parser yields a structurally-equal [`Ty`] (modulo whitespace and
153    /// canonical operator spacing). See the drift test in
154    /// `crates/harn-vm/tests/builtin_signature_text_drift.rs`.
155    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
156        match self {
157            Ty::Named(s) | Ty::Generic(s) => f.write_str(s),
158            Ty::Any => f.write_str("any"),
159            Ty::Never => f.write_str("never"),
160            Ty::Optional(inner) => write!(f, "{inner}?"),
161            Ty::Apply(name, args) => {
162                f.write_str(name)?;
163                f.write_str("<")?;
164                for (i, a) in args.iter().enumerate() {
165                    if i > 0 {
166                        f.write_str(", ")?;
167                    }
168                    write!(f, "{a}")?;
169                }
170                f.write_str(">")
171            }
172            Ty::Union(parts) => {
173                // Recover sig-grammar sugar so output round-trips through
174                // the proc-macro sig parser (which desugars `T?` and
175                // `number` into unions).
176                if let [inner, Ty::Named("nil")] = parts {
177                    if !matches!(inner, Ty::Named("nil")) {
178                        return write!(f, "{inner}?");
179                    }
180                }
181                if let [Ty::Named("int"), Ty::Named("float")] = parts {
182                    return f.write_str("number");
183                }
184                for (i, p) in parts.iter().enumerate() {
185                    if i > 0 {
186                        f.write_str(" | ")?;
187                    }
188                    write!(f, "{p}")?;
189                }
190                Ok(())
191            }
192            Ty::Fn(params, ret) => {
193                f.write_str("(")?;
194                for (i, p) in params.iter().enumerate() {
195                    if i > 0 {
196                        f.write_str(", ")?;
197                    }
198                    write!(f, "{p}")?;
199                }
200                write!(f, ") -> {ret}")
201            }
202            Ty::Shape(fields) => {
203                f.write_str("{")?;
204                for (i, fld) in fields.iter().enumerate() {
205                    if i > 0 {
206                        f.write_str(", ")?;
207                    }
208                    let name = fld.name;
209                    let ty = &fld.ty;
210                    write!(f, "{name}: {ty}")?;
211                    if fld.optional {
212                        f.write_str("?")?;
213                    }
214                }
215                f.write_str("}")
216            }
217            Ty::SchemaOf(t) => write!(f, "Schema<{t}>"),
218            Ty::LitInt(n) => write!(f, "{n}"),
219            Ty::LitString(s) => write!(f, "\"{s}\""),
220        }
221    }
222}
223
224impl BuiltinSignature {
225    /// Non-generic, fixed-arity builtin: no type parameters, no rest, no
226    /// where-clause bounds. Covers ~70% of the registry; lets each call
227    /// site stay on a single logical line.
228    pub const fn simple(name: &'static str, params: &'static [Param], returns: Ty) -> Self {
229        Self {
230            name,
231            params,
232            returns,
233            type_params: &[],
234            has_rest: false,
235            where_clauses: &[],
236        }
237    }
238
239    /// Non-generic builtin whose final parameter is variadic (rest).
240    /// Equivalent to [`Self::simple`] with `has_rest: true`.
241    pub const fn variadic(name: &'static str, params: &'static [Param], returns: Ty) -> Self {
242        Self {
243            name,
244            params,
245            returns,
246            type_params: &[],
247            has_rest: true,
248            where_clauses: &[],
249        }
250    }
251
252    /// Generic, fixed-arity builtin: declares type parameters, no rest,
253    /// no where-clause bounds. Use the struct literal directly when both
254    /// generics and where-clauses or rest are needed.
255    pub const fn generic(
256        name: &'static str,
257        type_params: &'static [&'static str],
258        params: &'static [Param],
259        returns: Ty,
260    ) -> Self {
261        Self {
262            name,
263            params,
264            returns,
265            type_params,
266            has_rest: false,
267            where_clauses: &[],
268        }
269    }
270
271    /// Number of required parameters (those without defaults).
272    pub fn required_params(&self) -> usize {
273        self.params.iter().filter(|p| !p.optional).count()
274    }
275
276    /// True when this builtin recognises `name` as one of its declared
277    /// generic type parameters.
278    pub fn is_type_param(&self, name: &str) -> bool {
279        self.type_params.contains(&name)
280    }
281
282    /// True when this builtin declares any generic type parameters.
283    pub fn is_generic(&self) -> bool {
284        !self.type_params.is_empty()
285    }
286
287    /// Materialize the type parameter names as owned strings (for use in
288    /// the type checker's existing scope/binding APIs which key off
289    /// `Vec<String>`).
290    pub fn type_param_names(&self) -> Vec<String> {
291        self.type_params.iter().map(|s| (*s).to_string()).collect()
292    }
293
294    /// Where-clause constraints as `(type_param, interface)` strings.
295    pub fn where_clause_strings(&self) -> Vec<(String, String)> {
296        self.where_clauses
297            .iter()
298            .map(|(tp, iface)| ((*tp).to_string(), (*iface).to_string()))
299            .collect()
300    }
301}
302
303impl core::fmt::Display for BuiltinSignature {
304    /// Render a parsed [`BuiltinSignature`] back into the `#[harn_builtin]`
305    /// `sig = "..."` grammar. Used by the drift test and by tooling that
306    /// wants a canonical string form of the signature regardless of how it
307    /// was originally typed.
308    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
309        if !self.type_params.is_empty() {
310            f.write_str("<")?;
311            for (i, tp) in self.type_params.iter().enumerate() {
312                if i > 0 {
313                    f.write_str(", ")?;
314                }
315                f.write_str(tp)?;
316            }
317            if !self.where_clauses.is_empty() {
318                f.write_str(" where ")?;
319                for (i, (tp, iface)) in self.where_clauses.iter().enumerate() {
320                    if i > 0 {
321                        f.write_str(", ")?;
322                    }
323                    write!(f, "{tp}: {iface}")?;
324                }
325            }
326            f.write_str("> ")?;
327        }
328        f.write_str(self.name)?;
329        f.write_str("(")?;
330        let last_idx = self.params.len().saturating_sub(1);
331        for (i, p) in self.params.iter().enumerate() {
332            if i > 0 {
333                f.write_str(", ")?;
334            }
335            if self.has_rest && i == last_idx {
336                f.write_str("...")?;
337            }
338            f.write_str(p.name)?;
339            if p.optional {
340                f.write_str("?")?;
341            }
342            let ty = &p.ty;
343            write!(f, ": {ty}")?;
344        }
345        let ret = &self.returns;
346        write!(f, ") -> {ret}")
347    }
348}
349
350/// Public view of one builtin used by `harn-lint` and other crates that need
351/// just identifier + return-type hints (no parameter types).
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub struct BuiltinMetadata {
354    pub name: &'static str,
355    pub return_types: &'static [&'static str],
356}
357
358// ---- Convenience constants ----
359//
360// Used pervasively in builtin signature literals to keep individual entries
361// terse. Add new constants here when a type appears repeatedly enough to
362// warrant a shorthand (avoid one-off shorthands).
363
364pub const TY_ANY: Ty = Ty::Any;
365pub const TY_BOOL: Ty = Ty::Named("bool");
366pub const TY_BYTES: Ty = Ty::Named("bytes");
367pub const TY_CLOSURE: Ty = Ty::Named("closure");
368pub const TY_DECIMAL: Ty = Ty::Named("decimal");
369pub const TY_DICT: Ty = Ty::Named("dict");
370pub const TY_DURATION: Ty = Ty::Named("duration");
371pub const TY_FLOAT: Ty = Ty::Named("float");
372pub const TY_INT: Ty = Ty::Named("int");
373pub const TY_LIST: Ty = Ty::Named("list");
374pub const TY_NEVER: Ty = Ty::Never;
375pub const TY_NIL: Ty = Ty::Named("nil");
376pub const TY_STRING: Ty = Ty::Named("string");
377
378/// `string | nil`.
379pub const TY_STRING_OR_NIL: Ty = Ty::Union(&[TY_STRING, TY_NIL]);
380/// `int | nil`.
381pub const TY_INT_OR_NIL: Ty = Ty::Union(&[TY_INT, TY_NIL]);
382/// `dict | nil`.
383pub const TY_DICT_OR_NIL: Ty = Ty::Union(&[TY_DICT, TY_NIL]);
384/// `bytes | nil`.
385pub const TY_BYTES_OR_NIL: Ty = Ty::Union(&[TY_BYTES, TY_NIL]);
386/// `int | float`.
387pub const TY_NUMBER: Ty = Ty::Union(&[TY_INT, TY_FLOAT]);
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    const APPLY_ARGS: &[Ty] = &[TY_DICT];
394    const FN_PARAMS: &[Ty] = &[TY_INT, TY_STRING];
395    const SHAPE_FIELDS: &[ShapeFieldDescriptor] = &[
396        ShapeFieldDescriptor::new("name", TY_STRING),
397        ShapeFieldDescriptor::optional("age", TY_INT),
398    ];
399
400    #[test]
401    fn ty_display_atomic_and_compound() {
402        assert_eq!(format!("{TY_INT}"), "int");
403        assert_eq!(format!("{TY_ANY}"), "any");
404        assert_eq!(format!("{TY_NEVER}"), "never");
405        // `T | nil` round-trips as `T?` (the sig grammar's optional sugar
406        // is desugared into a 2-element union, not `Ty::Optional`).
407        assert_eq!(format!("{TY_STRING_OR_NIL}"), "string?");
408        let opt_int = Ty::Optional(&TY_INT);
409        assert_eq!(format!("{opt_int}"), "int?");
410        // `int | float` round-trips as `number` (the predeclared shorthand).
411        assert_eq!(format!("{TY_NUMBER}"), "number");
412        let list_dict = Ty::Apply("list", APPLY_ARGS);
413        assert_eq!(format!("{list_dict}"), "list<dict>");
414        let lit_int = Ty::LitInt(42);
415        assert_eq!(format!("{lit_int}"), "42");
416        let lit_str = Ty::LitString("pass");
417        assert_eq!(format!("{lit_str}"), "\"pass\"");
418        let schema_t = Ty::SchemaOf("T");
419        assert_eq!(format!("{schema_t}"), "Schema<T>");
420        let fn_ty = Ty::Fn(FN_PARAMS, &TY_BOOL);
421        assert_eq!(format!("{fn_ty}"), "(int, string) -> bool");
422        let shape = Ty::Shape(SHAPE_FIELDS);
423        assert_eq!(format!("{shape}"), "{name: string, age: int?}");
424    }
425
426    const BASIC_PARAMS: &[Param] = &[Param::new("a", TY_DICT), Param::new("b", TY_DICT)];
427    const REST_PARAMS: &[Param] = &[Param::new("prefix", TY_STRING), Param::new("args", TY_ANY)];
428    const OPT_PARAMS: &[Param] = &[
429        Param::new("receipt", TY_DICT),
430        Param::optional("candidate", TY_ANY),
431    ];
432    const GENERIC_PARAMS: &[Param] = &[Param::new("schema", Ty::SchemaOf("T"))];
433
434    #[test]
435    fn signature_display_basic() {
436        let sig = BuiltinSignature::simple("deep_merge", BASIC_PARAMS, TY_DICT);
437        assert_eq!(format!("{sig}"), "deep_merge(a: dict, b: dict) -> dict");
438    }
439
440    #[test]
441    fn signature_display_with_optional_and_rest() {
442        let sig = BuiltinSignature {
443            name: "io_println",
444            params: REST_PARAMS,
445            returns: TY_NIL,
446            type_params: &[],
447            has_rest: true,
448            where_clauses: &[],
449        };
450        assert_eq!(
451            format!("{sig}"),
452            "io_println(prefix: string, ...args: any) -> nil"
453        );
454
455        let opt_sig =
456            BuiltinSignature::simple("lifecycle_replay_resume_input", OPT_PARAMS, TY_DICT);
457        assert_eq!(
458            format!("{opt_sig}"),
459            "lifecycle_replay_resume_input(receipt: dict, candidate?: any) -> dict"
460        );
461    }
462
463    #[test]
464    fn signature_display_with_generics_and_where() {
465        let sig = BuiltinSignature {
466            name: "schema_parse",
467            params: GENERIC_PARAMS,
468            returns: Ty::Generic("T"),
469            type_params: &["T"],
470            has_rest: false,
471            where_clauses: &[("T", "Decode")],
472        };
473        assert_eq!(
474            format!("{sig}"),
475            "<T where T: Decode> schema_parse(schema: Schema<T>) -> T"
476        );
477    }
478}