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