Skip to main content

harn_parser/builtin_signatures/
lookup.rs

1//! Public lookup helpers over the unified [`BuiltinSignature`] registry.
2//!
3//! Both the type checker (this crate) and the runtime (`harn-vm`) consume
4//! these helpers. Generic builtins declare their type parameters via
5//! [`BuiltinSignature::type_params`] and use [`Ty::Generic`]/[`Ty::SchemaOf`]
6//! in param/return positions; the type checker materializes those at each
7//! call site against the surrounding scope.
8
9use crate::ast::TypeExpr;
10use harn_builtin_meta::{BuiltinExposure, CapabilityId};
11
12use super::signatures;
13use super::{BuiltinMetadata, BuiltinSignature, Ty, TyExt};
14
15/// Linear scan of the installed slice plus the static groups for a given
16/// name. Installed entries win when both sides carry the same name (the
17/// `#[harn_builtin]`-emitted signature shadows any legacy static
18/// duplicate). O(N+M) per call where N+M ≈ 600 — well below any
19/// typecheck hot path, and explicitly NOT cached so installs after the
20/// first call are picked up without leaking an old merged slice.
21pub fn lookup(name: &str) -> Option<&'static BuiltinSignature> {
22    for entry in harn_builtin_registry::installed_manifest() {
23        if entry.name == name {
24            return matches!(
25                entry.contract.exposure,
26                BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
27            )
28            .then_some(entry.signature);
29        }
30    }
31    for group in signatures::groups() {
32        for sig in group {
33            if sig.name == name {
34                return Some(sig);
35            }
36        }
37    }
38    None
39}
40
41/// Resolve the signature paired with one capability method contract.
42pub fn lookup_capability_method(
43    capability: CapabilityId,
44    method: &str,
45) -> Option<&'static BuiltinSignature> {
46    capability_method_entry(capability.field_name(), method).map(|entry| entry.signature)
47}
48
49/// Resolve the single manifest entry that owns a `harness.<field>.<method>`
50/// call. Consumers that need effects or the internal dispatch name use this
51/// rather than reconstructing either from strings.
52pub fn capability_method_entry(
53    field: &str,
54    method: &str,
55) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
56    let capability = CapabilityId::from_field_name(field)?;
57    harn_builtin_registry::installed_manifest()
58        .iter()
59        .copied()
60        .find(|entry| {
61            matches!(
62                entry.contract.exposure,
63                BuiltinExposure::HarnessMethod {
64                    capability: candidate,
65                    method: candidate_method,
66                } if candidate == capability && candidate_method == method
67            )
68        })
69        .or_else(|| harn_capability_contracts::capability_method_entry(field, method))
70}
71
72/// Is `name` a builtin known to the parser?
73pub fn is_builtin(name: &str) -> bool {
74    lookup(name).is_some()
75}
76
77/// Every builtin name. Installed names come first, then any static-only
78/// names that aren't shadowed by installed entries. Output is NOT
79/// alphabetically sorted (callers that need that re-sort themselves).
80pub fn iter_builtin_names() -> impl Iterator<Item = &'static str> {
81    let installed: Vec<_> = harn_builtin_registry::installed_manifest()
82        .into_iter()
83        .filter(|entry| {
84            matches!(
85                entry.contract.exposure,
86                BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
87            )
88        })
89        .collect();
90    let installed_names: std::collections::HashSet<&'static str> =
91        harn_builtin_registry::installed_manifest()
92            .into_iter()
93            .map(|entry| entry.name)
94            .collect();
95    installed.into_iter().map(|entry| entry.name).chain(
96        signatures::groups()
97            .into_iter()
98            .flat_map(|g| g.iter())
99            .filter(move |s| !installed_names.contains(s.name))
100            .map(|s| s.name),
101    )
102}
103
104/// Names that come *only* from the hand-written static fallback tables
105/// (`signatures::groups()`), independent of whatever the driver installed.
106///
107/// Exposed so cross-crate drift guards (see the builtin-registry alignment
108/// test in `harn-vm`) can assert the static tables never overlap with
109/// `#[harn_builtin]`-published or `runtime_only` macro builtins — the exact
110/// duplication that let LLM config signatures silently drift before the
111/// shapes-in-`harn-builtin-meta` migration.
112pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
113    signatures::groups()
114        .into_iter()
115        .flat_map(|g| g.iter())
116        .map(|s| s.name)
117}
118
119/// Iterate over every builtin's name and statically-known return-type
120/// strings. Used by `harn-lint` and other consumers that want a
121/// lightweight "what does this builtin return" view without bringing in
122/// the full type IR.
123pub fn iter_builtin_metadata() -> impl Iterator<Item = BuiltinMetadata> {
124    let installed: Vec<_> = harn_builtin_registry::installed_manifest()
125        .into_iter()
126        .filter(|entry| {
127            matches!(
128                entry.contract.exposure,
129                BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
130            )
131        })
132        .collect();
133    let installed_names: std::collections::HashSet<&'static str> =
134        harn_builtin_registry::installed_manifest()
135            .into_iter()
136            .map(|entry| entry.name)
137            .collect();
138    installed
139        .into_iter()
140        .map(|entry| BuiltinMetadata {
141            name: entry.name,
142            return_types: builtin_return_type_names(entry.signature),
143        })
144        .chain(
145            signatures::groups()
146                .into_iter()
147                .flat_map(|g| g.iter())
148                .filter(move |s| !installed_names.contains(s.name))
149                .map(|sig| BuiltinMetadata {
150                    name: sig.name,
151                    return_types: builtin_return_type_names(sig),
152                }),
153        )
154}
155
156/// Statically-known return type for `name`, materialized as a [`TypeExpr`].
157/// Returns `None` for unknown names AND for builtins whose return type is
158/// genuinely dynamic ([`Ty::Any`]).
159pub fn builtin_return_type(name: &str) -> Option<TypeExpr> {
160    let sig = lookup(name)?;
161    if sig.returns.is_any() {
162        return None;
163    }
164    Some(sig.returns.to_type_expr())
165}
166
167/// Returns true if this builtin produces an untyped/opaque value that
168/// should be validated before field access in strict types mode.
169///
170/// This is the same set the linter's `untyped-dict-access` rule treats
171/// as boundary sources — JSON parsing, HTTP responses, LLM outputs,
172/// host capability calls, etc.
173pub fn is_untyped_boundary_source(name: &str) -> bool {
174    matches!(
175        name,
176        "json_parse"
177            | "json_extract"
178            | "yaml_parse"
179            | "toml_parse"
180            | "llm_call"
181            | "llm_call_safe"
182            | "llm_completion"
183            | "http_get"
184            | "http_post"
185            | "http_put"
186            | "http_patch"
187            | "http_delete"
188            | "http_download"
189            | "http_request"
190            | "http_session_request"
191            | "http_stream_info"
192            | "sse_receive"
193            | "sse_server_mock_receive"
194            | "sse_server_response"
195            | "sse_server_status"
196            | "websocket_accept"
197            | "websocket_receive"
198            | "host_call"
199            | "connector_call"
200            | "host_tool_call"
201    )
202}
203
204/// Convert the signature's return type to a tiny `&'static [&'static str]`
205/// view used by `BuiltinMetadata` consumers (linter, LSP) that don't
206/// pull in the full type IR. Only basic primitive names and the common
207/// `T | nil` unions are exposed; everything else returns an empty slice
208/// so callers know to consult [`builtin_return_type`] instead.
209fn builtin_return_type_names(sig: &BuiltinSignature) -> &'static [&'static str] {
210    match &sig.returns {
211        Ty::Named(name) => match *name {
212            "bool" => &["bool"],
213            "bytes" => &["bytes"],
214            "dict" => &["dict"],
215            "float" => &["float"],
216            "int" => &["int"],
217            "list" => &["list"],
218            "nil" => &["nil"],
219            "string" => &["string"],
220            _ => &[],
221        },
222        Ty::Union(members) => match *members {
223            [Ty::Named("string"), Ty::Named("nil")] => &["string", "nil"],
224            [Ty::Named("nil"), Ty::Named("string")] => &["string", "nil"],
225            [Ty::Named("int"), Ty::Named("nil")] => &["int", "nil"],
226            [Ty::Named("nil"), Ty::Named("int")] => &["int", "nil"],
227            [Ty::Named("dict"), Ty::Named("nil")] => &["dict", "nil"],
228            [Ty::Named("nil"), Ty::Named("dict")] => &["dict", "nil"],
229            [Ty::Named("bytes"), Ty::Named("nil")] => &["bytes", "nil"],
230            [Ty::Named("nil"), Ty::Named("bytes")] => &["bytes", "nil"],
231            _ => &[],
232        },
233        Ty::Never => &["never"],
234        _ => &[],
235    }
236}