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