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