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