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