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()
80        .into_iter()
81        .filter(|entry| {
82            matches!(
83                entry.contract.exposure,
84                BuiltinExposure::HarnessMethod { method, .. } if method == name
85            )
86        });
87    let entry = matches.next()?;
88    matches.next().is_none().then_some(entry)
89}
90
91/// Resolve a pre-cutover ambient global whose typed contract is published under
92/// the hidden `__cap_<name>` spelling (for example `runtime_context_set` →
93/// `__cap_runtime_context_set` for `harness.runtime.context_set`).
94pub fn legacy_ambient_cap_global_entry(
95    name: &str,
96) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
97    ambient_harness_method_entries()
98        .into_iter()
99        .find(|entry| entry.name.strip_prefix("__cap_") == Some(name))
100}
101
102/// Resolve a privileged-wire builtin published as `__<name>` (for example
103/// ambient `security_policy` → `__security_policy`).
104pub fn legacy_privileged_wire_entry(
105    name: &str,
106) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
107    harn_builtin_registry::installed_manifest()
108        .into_iter()
109        .find(|entry| {
110            matches!(entry.contract.exposure, BuiltinExposure::PrivilegedWire)
111                && entry.name.strip_prefix("__") == Some(name)
112        })
113}
114
115/// Canonical runtime builtin name for an ambient call site under the legacy
116/// bridge.
117///
118/// Only rewrite when the runtime registers a different spelling than the
119/// source call. Privileged-wire builtins publish as `__name`. Host internals
120/// (`__host_*`) and capability `__cap_*` contracts keep their short ambient
121/// names; the VM projects those globals under the ambient bridge.
122pub fn legacy_ambient_runtime_name(name: &str) -> Option<&'static str> {
123    if let Some(target) = crate::legacy_builtin_alias_target(name) {
124        return Some(target);
125    }
126    legacy_privileged_wire_entry(name).map(|entry| entry.name)
127}
128
129fn ambient_harness_method_entries() -> Vec<&'static harn_builtin_registry::BuiltinManifestEntry> {
130    // Once the CLI/runtime installs the process manifest, prefer it alone.
131    // Chaining the static capability-contracts table on top duplicates every
132    // `__cap_*` method and makes `legacy_capability_method_entry` treat unique
133    // owners as ambiguous (two identical matches), which breaks ambient check.
134    let installed = harn_builtin_registry::installed_manifest();
135    if installed.is_empty() {
136        harn_capability_contracts::manifest().to_vec()
137    } else {
138        installed
139    }
140}
141
142/// Resolve the signature paired with one capability method contract.
143pub fn lookup_capability_method(
144    capability: CapabilityId,
145    method: &str,
146) -> Option<&'static BuiltinSignature> {
147    capability_method_entry(capability.field_name(), method).map(|entry| entry.signature)
148}
149
150/// Resolve the single manifest entry that owns a `harness.<field>.<method>`
151/// call. Consumers that need effects or the internal dispatch name use this
152/// rather than reconstructing either from strings.
153pub fn capability_method_entry(
154    field: &str,
155    method: &str,
156) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
157    let capability = CapabilityId::from_field_name(field)?;
158    harn_builtin_registry::installed_manifest()
159        .iter()
160        .copied()
161        .find(|entry| {
162            matches!(
163                entry.contract.exposure,
164                BuiltinExposure::HarnessMethod {
165                    capability: candidate,
166                    method: candidate_method,
167                } if candidate == capability && candidate_method == method
168            )
169        })
170        .or_else(|| harn_capability_contracts::capability_method_entry(field, method))
171}
172
173/// Is `name` a builtin known to the parser?
174pub fn is_builtin(name: &str) -> bool {
175    lookup(name).is_some()
176        || (crate::legacy_ambient_capabilities_enabled()
177            && crate::is_registered_legacy_hostlib_name(name))
178}
179
180pub fn is_builtin_with_privileged_wire(name: &str, allow_privileged_wire: bool) -> bool {
181    lookup_with_privileged_wire(name, allow_privileged_wire).is_some()
182        || (crate::legacy_ambient_capabilities_enabled()
183            && crate::is_registered_legacy_hostlib_name(name))
184}
185
186/// Every builtin name. Installed names come first, then any static-only
187/// names that aren't shadowed by installed entries. Output is NOT
188/// alphabetically sorted (callers that need that re-sort themselves).
189pub fn iter_builtin_names() -> impl Iterator<Item = &'static str> {
190    let installed: Vec<_> = harn_builtin_registry::installed_manifest()
191        .into_iter()
192        .filter(|entry| {
193            matches!(
194                entry.contract.exposure,
195                BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
196            )
197        })
198        .collect();
199    let installed_names: std::collections::HashSet<&'static str> =
200        harn_builtin_registry::installed_manifest()
201            .into_iter()
202            .map(|entry| entry.name)
203            .collect();
204    installed.into_iter().map(|entry| entry.name).chain(
205        signatures::groups()
206            .into_iter()
207            .flat_map(|g| g.iter())
208            .filter(move |s| !installed_names.contains(s.name))
209            .map(|s| s.name),
210    )
211}
212
213/// Names that come *only* from the hand-written static fallback tables
214/// (`signatures::groups()`), independent of whatever the driver installed.
215///
216/// Exposed so cross-crate drift guards (see the builtin-registry alignment
217/// test in `harn-vm`) can assert the static tables never overlap with
218/// `#[harn_builtin]`-published or `runtime_only` macro builtins — the exact
219/// duplication that let LLM config signatures silently drift before the
220/// shapes-in-`harn-builtin-meta` migration.
221pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
222    signatures::groups()
223        .into_iter()
224        .flat_map(|g| g.iter())
225        .map(|s| s.name)
226}
227
228/// Iterate over every builtin's name and statically-known return-type
229/// strings. Used by `harn-lint` and other consumers that want a
230/// lightweight "what does this builtin return" view without bringing in
231/// the full type IR.
232pub fn iter_builtin_metadata() -> impl Iterator<Item = BuiltinMetadata> {
233    let installed: Vec<_> = harn_builtin_registry::installed_manifest()
234        .into_iter()
235        .filter(|entry| {
236            matches!(
237                entry.contract.exposure,
238                BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
239            )
240        })
241        .collect();
242    let installed_names: std::collections::HashSet<&'static str> =
243        harn_builtin_registry::installed_manifest()
244            .into_iter()
245            .map(|entry| entry.name)
246            .collect();
247    installed
248        .into_iter()
249        .map(|entry| BuiltinMetadata {
250            name: entry.name,
251            return_types: builtin_return_type_names(entry.signature),
252        })
253        .chain(
254            signatures::groups()
255                .into_iter()
256                .flat_map(|g| g.iter())
257                .filter(move |s| !installed_names.contains(s.name))
258                .map(|sig| BuiltinMetadata {
259                    name: sig.name,
260                    return_types: builtin_return_type_names(sig),
261                }),
262        )
263}
264
265/// Statically-known return type for `name`, materialized as a [`TypeExpr`].
266/// Returns `None` for unknown names AND for builtins whose return type is
267/// genuinely dynamic ([`Ty::Any`]).
268pub fn builtin_return_type(name: &str) -> Option<TypeExpr> {
269    let sig = lookup(name)?;
270    if sig.returns.is_any() {
271        return None;
272    }
273    Some(sig.returns.to_type_expr())
274}
275
276/// Returns true if this builtin produces an untyped/opaque value that
277/// should be validated before field access in strict types mode.
278///
279/// This is the same set the linter's `untyped-dict-access` rule treats
280/// as boundary sources — JSON parsing, HTTP responses, LLM outputs,
281/// host capability calls, etc.
282pub fn is_untyped_boundary_source(name: &str) -> bool {
283    matches!(
284        name,
285        "json_parse"
286            | "json_extract"
287            | "yaml_parse"
288            | "toml_parse"
289            | "llm_call"
290            | "llm_call_safe"
291            | "llm_completion"
292            | "http_get"
293            | "http_post"
294            | "http_put"
295            | "http_patch"
296            | "http_delete"
297            | "http_download"
298            | "http_request"
299            | "http_session_request"
300            | "http_stream_info"
301            | "sse_receive"
302            | "sse_server_mock_receive"
303            | "sse_server_response"
304            | "sse_server_status"
305            | "websocket_accept"
306            | "websocket_receive"
307            | "host_call"
308            | "connector_call"
309            | "host_tool_call"
310    )
311}
312
313/// Convert the signature's return type to a tiny `&'static [&'static str]`
314/// view used by `BuiltinMetadata` consumers (linter, LSP) that don't
315/// pull in the full type IR. Only basic primitive names and the common
316/// `T | nil` unions are exposed; everything else returns an empty slice
317/// so callers know to consult [`builtin_return_type`] instead.
318fn builtin_return_type_names(sig: &BuiltinSignature) -> &'static [&'static str] {
319    match &sig.returns {
320        Ty::Named(name) => match *name {
321            "bool" => &["bool"],
322            "bytes" => &["bytes"],
323            "dict" => &["dict"],
324            "float" => &["float"],
325            "int" => &["int"],
326            "list" => &["list"],
327            "nil" => &["nil"],
328            "string" => &["string"],
329            _ => &[],
330        },
331        Ty::Union(members) => match *members {
332            [Ty::Named("string"), Ty::Named("nil")] => &["string", "nil"],
333            [Ty::Named("nil"), Ty::Named("string")] => &["string", "nil"],
334            [Ty::Named("int"), Ty::Named("nil")] => &["int", "nil"],
335            [Ty::Named("nil"), Ty::Named("int")] => &["int", "nil"],
336            [Ty::Named("dict"), Ty::Named("nil")] => &["dict", "nil"],
337            [Ty::Named("nil"), Ty::Named("dict")] => &["dict", "nil"],
338            [Ty::Named("bytes"), Ty::Named("nil")] => &["bytes", "nil"],
339            [Ty::Named("nil"), Ty::Named("bytes")] => &["bytes", "nil"],
340            _ => &[],
341        },
342        Ty::Never => &["never"],
343        _ => &[],
344    }
345}
346
347#[cfg(test)]
348mod ambient_install_regression {
349    use super::*;
350
351    #[test]
352    fn installed_manifest_does_not_shadow_ambient_capability_methods() {
353        std::env::set_var("HARN_LEGACY_AMBIENT_CAPABILITIES", "1");
354        assert!(
355            is_builtin("store_set"),
356            "capability-contracts fallback must resolve ambient store_set"
357        );
358
359        // Project the same contracts the CLI installs before `harn check`.
360        let entries: &'static [&'static harn_builtin_registry::BuiltinManifestEntry] = Box::leak(
361            harn_capability_contracts::manifest()
362                .to_vec()
363                .into_boxed_slice(),
364        );
365        harn_builtin_registry::install_builtin_manifest(entries);
366
367        assert!(
368            is_builtin("store_set"),
369            "after manifest install, ambient store_set must still resolve uniquely"
370        );
371        assert!(
372            legacy_capability_method_entry("store_set").is_some(),
373            "legacy_capability_method_entry must stay unique after install"
374        );
375    }
376}