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/// Resolve the installed name index, then fall back to static signature groups.
16/// Installed entries win when both sides carry the same name (the
17/// `#[harn_builtin]`-emitted signature shadows any legacy static duplicate).
18/// Runtime validation calls this for every builtin invocation, so the owning
19/// registry lookup must not materialize or linearly scan its whole manifest.
20pub fn lookup(name: &str) -> Option<&'static BuiltinSignature> {
21    if let Some(entry) = harn_builtin_registry::builtin_entry(name) {
22        return matches!(
23            entry.contract.exposure,
24            BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
25        )
26        .then_some(entry.signature)
27        .or_else(|| {
28            (crate::legacy_ambient_capabilities_enabled()
29                && matches!(
30                    entry.contract.exposure,
31                    BuiltinExposure::HarnessMethod { .. }
32                        | BuiltinExposure::PrivilegedWire
33                        | BuiltinExposure::RuntimeInternal
34                ))
35            .then_some(entry.signature)
36        });
37    }
38    if crate::legacy_ambient_capabilities_enabled() {
39        if let Some(entry) = legacy_capability_method_entry(name) {
40            return Some(entry.signature);
41        }
42        if let Some(canonical) = crate::legacy_builtin_alias_target(name) {
43            return lookup(canonical);
44        }
45    }
46    for group in signatures::groups() {
47        for sig in group {
48            if sig.name == name {
49                return Some(sig);
50            }
51        }
52    }
53    None
54}
55
56/// Resolve an unqualified legacy method name only when the typed manifest has
57/// exactly one owning Harness capability. Ambiguous method spellings remain
58/// unavailable rather than selecting authority by registration order.
59pub fn legacy_capability_method_entry(
60    name: &str,
61) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
62    let mut matches = harn_builtin_registry::installed_manifest()
63        .into_iter()
64        .filter(|entry| {
65            matches!(
66                entry.contract.exposure,
67                BuiltinExposure::HarnessMethod { method, .. } if method == name
68            )
69        });
70    let entry = matches.next()?;
71    matches.next().is_none().then_some(entry)
72}
73
74/// Resolve the signature paired with one capability method contract.
75pub fn lookup_capability_method(
76    capability: CapabilityId,
77    method: &str,
78) -> Option<&'static BuiltinSignature> {
79    capability_method_entry(capability.field_name(), method).map(|entry| entry.signature)
80}
81
82/// Resolve the single manifest entry that owns a `harness.<field>.<method>`
83/// call. Consumers that need effects or the internal dispatch name use this
84/// rather than reconstructing either from strings.
85pub fn capability_method_entry(
86    field: &str,
87    method: &str,
88) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
89    let capability = CapabilityId::from_field_name(field)?;
90    harn_builtin_registry::installed_manifest()
91        .iter()
92        .copied()
93        .find(|entry| {
94            matches!(
95                entry.contract.exposure,
96                BuiltinExposure::HarnessMethod {
97                    capability: candidate,
98                    method: candidate_method,
99                } if candidate == capability && candidate_method == method
100            )
101        })
102        .or_else(|| harn_capability_contracts::capability_method_entry(field, method))
103}
104
105/// Is `name` a builtin known to the parser?
106pub fn is_builtin(name: &str) -> bool {
107    lookup(name).is_some()
108        || (crate::legacy_ambient_capabilities_enabled()
109            && crate::is_registered_legacy_hostlib_name(name))
110}
111
112/// Every builtin name. Installed names come first, then any static-only
113/// names that aren't shadowed by installed entries. Output is NOT
114/// alphabetically sorted (callers that need that re-sort themselves).
115pub fn iter_builtin_names() -> impl Iterator<Item = &'static str> {
116    let installed: Vec<_> = harn_builtin_registry::installed_manifest()
117        .into_iter()
118        .filter(|entry| {
119            matches!(
120                entry.contract.exposure,
121                BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
122            )
123        })
124        .collect();
125    let installed_names: std::collections::HashSet<&'static str> =
126        harn_builtin_registry::installed_manifest()
127            .into_iter()
128            .map(|entry| entry.name)
129            .collect();
130    installed.into_iter().map(|entry| entry.name).chain(
131        signatures::groups()
132            .into_iter()
133            .flat_map(|g| g.iter())
134            .filter(move |s| !installed_names.contains(s.name))
135            .map(|s| s.name),
136    )
137}
138
139/// Names that come *only* from the hand-written static fallback tables
140/// (`signatures::groups()`), independent of whatever the driver installed.
141///
142/// Exposed so cross-crate drift guards (see the builtin-registry alignment
143/// test in `harn-vm`) can assert the static tables never overlap with
144/// `#[harn_builtin]`-published or `runtime_only` macro builtins — the exact
145/// duplication that let LLM config signatures silently drift before the
146/// shapes-in-`harn-builtin-meta` migration.
147pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
148    signatures::groups()
149        .into_iter()
150        .flat_map(|g| g.iter())
151        .map(|s| s.name)
152}
153
154/// Iterate over every builtin's name and statically-known return-type
155/// strings. Used by `harn-lint` and other consumers that want a
156/// lightweight "what does this builtin return" view without bringing in
157/// the full type IR.
158pub fn iter_builtin_metadata() -> impl Iterator<Item = BuiltinMetadata> {
159    let installed: Vec<_> = harn_builtin_registry::installed_manifest()
160        .into_iter()
161        .filter(|entry| {
162            matches!(
163                entry.contract.exposure,
164                BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
165            )
166        })
167        .collect();
168    let installed_names: std::collections::HashSet<&'static str> =
169        harn_builtin_registry::installed_manifest()
170            .into_iter()
171            .map(|entry| entry.name)
172            .collect();
173    installed
174        .into_iter()
175        .map(|entry| BuiltinMetadata {
176            name: entry.name,
177            return_types: builtin_return_type_names(entry.signature),
178        })
179        .chain(
180            signatures::groups()
181                .into_iter()
182                .flat_map(|g| g.iter())
183                .filter(move |s| !installed_names.contains(s.name))
184                .map(|sig| BuiltinMetadata {
185                    name: sig.name,
186                    return_types: builtin_return_type_names(sig),
187                }),
188        )
189}
190
191/// Statically-known return type for `name`, materialized as a [`TypeExpr`].
192/// Returns `None` for unknown names AND for builtins whose return type is
193/// genuinely dynamic ([`Ty::Any`]).
194pub fn builtin_return_type(name: &str) -> Option<TypeExpr> {
195    let sig = lookup(name)?;
196    if sig.returns.is_any() {
197        return None;
198    }
199    Some(sig.returns.to_type_expr())
200}
201
202/// Returns true if this builtin produces an untyped/opaque value that
203/// should be validated before field access in strict types mode.
204///
205/// This is the same set the linter's `untyped-dict-access` rule treats
206/// as boundary sources — JSON parsing, HTTP responses, LLM outputs,
207/// host capability calls, etc.
208pub fn is_untyped_boundary_source(name: &str) -> bool {
209    matches!(
210        name,
211        "json_parse"
212            | "json_extract"
213            | "yaml_parse"
214            | "toml_parse"
215            | "llm_call"
216            | "llm_call_safe"
217            | "llm_completion"
218            | "http_get"
219            | "http_post"
220            | "http_put"
221            | "http_patch"
222            | "http_delete"
223            | "http_download"
224            | "http_request"
225            | "http_session_request"
226            | "http_stream_info"
227            | "sse_receive"
228            | "sse_server_mock_receive"
229            | "sse_server_response"
230            | "sse_server_status"
231            | "websocket_accept"
232            | "websocket_receive"
233            | "host_call"
234            | "connector_call"
235            | "host_tool_call"
236    )
237}
238
239/// Convert the signature's return type to a tiny `&'static [&'static str]`
240/// view used by `BuiltinMetadata` consumers (linter, LSP) that don't
241/// pull in the full type IR. Only basic primitive names and the common
242/// `T | nil` unions are exposed; everything else returns an empty slice
243/// so callers know to consult [`builtin_return_type`] instead.
244fn builtin_return_type_names(sig: &BuiltinSignature) -> &'static [&'static str] {
245    match &sig.returns {
246        Ty::Named(name) => match *name {
247            "bool" => &["bool"],
248            "bytes" => &["bytes"],
249            "dict" => &["dict"],
250            "float" => &["float"],
251            "int" => &["int"],
252            "list" => &["list"],
253            "nil" => &["nil"],
254            "string" => &["string"],
255            _ => &[],
256        },
257        Ty::Union(members) => match *members {
258            [Ty::Named("string"), Ty::Named("nil")] => &["string", "nil"],
259            [Ty::Named("nil"), Ty::Named("string")] => &["string", "nil"],
260            [Ty::Named("int"), Ty::Named("nil")] => &["int", "nil"],
261            [Ty::Named("nil"), Ty::Named("int")] => &["int", "nil"],
262            [Ty::Named("dict"), Ty::Named("nil")] => &["dict", "nil"],
263            [Ty::Named("nil"), Ty::Named("dict")] => &["dict", "nil"],
264            [Ty::Named("bytes"), Ty::Named("nil")] => &["bytes", "nil"],
265            [Ty::Named("nil"), Ty::Named("bytes")] => &["bytes", "nil"],
266            _ => &[],
267        },
268        Ty::Never => &["never"],
269        _ => &[],
270    }
271}