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