harn-parser 0.10.116

Parser, AST, and type checker for the Harn programming language
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! Public lookup helpers over the unified [`BuiltinSignature`] registry.
//!
//! Both the type checker (this crate) and the runtime (`harn-vm`) consume
//! these helpers. Generic builtins declare their type parameters via
//! [`BuiltinSignature::type_params`] and use [`Ty::Generic`]/[`Ty::SchemaOf`]
//! in param/return positions; the type checker materializes those at each
//! call site against the surrounding scope.

use crate::ast::TypeExpr;
use harn_builtin_meta::{BuiltinExposure, CapabilityId};

use super::signatures;
use super::{BuiltinMetadata, BuiltinSignature, Ty, TyExt};

/// Resolve the installed name index, then fall back to static signature groups.
/// Installed entries win when both sides carry the same name (the
/// `#[harn_builtin]`-emitted signature shadows any legacy static duplicate).
/// Runtime validation calls this for every builtin invocation, so the owning
/// registry lookup must not materialize or linearly scan its whole manifest.
pub fn lookup(name: &str) -> Option<&'static BuiltinSignature> {
    lookup_with_privileged_wire(name, false)
}

/// Resolve a builtin for an explicitly trusted host-dispatch compilation.
/// This widens only `PrivilegedWire`; it does not restore legacy Harness
/// methods or runtime-internal names as ambient globals.
pub fn lookup_with_privileged_wire(
    name: &str,
    allow_privileged_wire: bool,
) -> Option<&'static BuiltinSignature> {
    if let Some(entry) = harn_builtin_registry::builtin_entry(name) {
        return matches!(
            entry.contract.exposure,
            BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
        )
        .then_some(entry.signature)
        .or_else(|| {
            (allow_privileged_wire && entry.contract.exposure == BuiltinExposure::PrivilegedWire)
                .then_some(entry.signature)
        })
        .or_else(|| {
            (crate::legacy_ambient_capabilities_enabled()
                && matches!(
                    entry.contract.exposure,
                    BuiltinExposure::HarnessMethod { .. }
                        | BuiltinExposure::PrivilegedWire
                        | BuiltinExposure::StdlibInternal
                        | BuiltinExposure::RuntimeInternal
                ))
            .then_some(entry.signature)
        });
    }
    if crate::legacy_ambient_capabilities_enabled() {
        if let Some(entry) = legacy_capability_method_entry(name) {
            return Some(entry.signature);
        }
        if let Some(entry) = legacy_ambient_cap_global_entry(name) {
            return Some(entry.signature);
        }
        if let Some(canonical) = crate::legacy_builtin_alias_target(name) {
            return lookup(canonical);
        }
    }
    static_signature_index().get(name).copied()
}

/// Name index over the hand-written static fallback tables.
///
/// The static groups hold several hundred signatures, and the fallback fires
/// for *every* name the registry does not know — including every user-defined
/// function call the type checker resolves — so a linear scan of every group
/// per miss was a measurable slice of whole-file typechecking. The tables are
/// `'static`, so one lazily-built index serves every lookup.
fn static_signature_index(
) -> &'static std::collections::HashMap<&'static str, &'static BuiltinSignature> {
    static INDEX: std::sync::OnceLock<
        std::collections::HashMap<&'static str, &'static BuiltinSignature>,
    > = std::sync::OnceLock::new();
    INDEX.get_or_init(|| {
        let mut index = std::collections::HashMap::new();
        for group in signatures::groups() {
            for sig in group {
                // First writer wins, matching the previous scan order.
                index.entry(sig.name).or_insert(sig);
            }
        }
        index
    })
}

/// Resolve an unqualified legacy method name only when the typed manifest has
/// exactly one owning Harness capability. Ambiguous method spellings remain
/// unavailable rather than selecting authority by registration order.
pub fn legacy_capability_method_entry(
    name: &str,
) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
    let mut matches = ambient_harness_method_entries()
        .into_iter()
        .filter(|entry| {
            matches!(
                entry.contract.exposure,
                BuiltinExposure::HarnessMethod { method, .. } if method == name
            )
        });
    let entry = matches.next()?;
    matches.next().is_none().then_some(entry)
}

/// Resolve a pre-cutover ambient global whose typed contract is published under
/// the hidden `__cap_<name>` spelling (for example `runtime_context_set` →
/// `__cap_runtime_context_set` for `harness.runtime.context_set`).
pub fn legacy_ambient_cap_global_entry(
    name: &str,
) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
    ambient_harness_method_entries()
        .into_iter()
        .find(|entry| entry.name.strip_prefix("__cap_") == Some(name))
}

/// Resolve a privileged-wire builtin published as `__<name>` (for example
/// ambient `security_policy` → `__security_policy`).
pub fn legacy_privileged_wire_entry(
    name: &str,
) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
    harn_builtin_registry::installed_manifest()
        .into_iter()
        .find(|entry| {
            matches!(entry.contract.exposure, BuiltinExposure::PrivilegedWire)
                && entry.name.strip_prefix("__") == Some(name)
        })
}

/// Canonical runtime builtin name for an ambient call site under the legacy
/// bridge.
///
/// Only rewrite when the runtime registers a different spelling than the
/// source call. Privileged-wire builtins publish as `__name`. Host internals
/// (`__host_*`) and capability `__cap_*` contracts keep their short ambient
/// names; the VM projects those globals under the ambient bridge.
pub fn legacy_ambient_runtime_name(name: &str) -> Option<&'static str> {
    if let Some(target) = crate::legacy_builtin_alias_target(name) {
        return Some(target);
    }
    legacy_privileged_wire_entry(name).map(|entry| entry.name)
}

fn ambient_harness_method_entries() -> Vec<&'static harn_builtin_registry::BuiltinManifestEntry> {
    // Once the CLI/runtime installs the process manifest, prefer it alone.
    // Chaining the static capability-contracts table on top duplicates every
    // `__cap_*` method and makes `legacy_capability_method_entry` treat unique
    // owners as ambiguous (two identical matches), which breaks ambient check.
    let installed = harn_builtin_registry::installed_manifest();
    if installed.is_empty() {
        harn_capability_contracts::manifest().to_vec()
    } else {
        installed
    }
}

/// Resolve the signature paired with one capability method contract.
pub fn lookup_capability_method(
    capability: CapabilityId,
    method: &str,
) -> Option<&'static BuiltinSignature> {
    capability_method_entry(capability.field_name(), method).map(|entry| entry.signature)
}

/// Resolve the single manifest entry that owns a `harness.<field>.<method>`
/// call. Consumers that need effects or the internal dispatch name use this
/// rather than reconstructing either from strings.
pub fn capability_method_entry(
    field: &str,
    method: &str,
) -> Option<&'static harn_builtin_registry::BuiltinManifestEntry> {
    let capability = CapabilityId::from_field_name(field)?;
    harn_builtin_registry::installed_manifest()
        .iter()
        .copied()
        .find(|entry| {
            matches!(
                entry.contract.exposure,
                BuiltinExposure::HarnessMethod {
                    capability: candidate,
                    method: candidate_method,
                } if candidate == capability && candidate_method == method
            )
        })
        .or_else(|| harn_capability_contracts::capability_method_entry(field, method))
}

/// Is `name` a builtin known to the parser?
pub fn is_builtin(name: &str) -> bool {
    lookup(name).is_some()
        || (crate::legacy_ambient_capabilities_enabled()
            && crate::is_registered_legacy_hostlib_name(name))
}

pub fn is_builtin_with_privileged_wire(name: &str, allow_privileged_wire: bool) -> bool {
    lookup_with_privileged_wire(name, allow_privileged_wire).is_some()
        || (crate::legacy_ambient_capabilities_enabled()
            && crate::is_registered_legacy_hostlib_name(name))
}

/// Every builtin name. Installed names come first, then any static-only
/// names that aren't shadowed by installed entries. Output is NOT
/// alphabetically sorted (callers that need that re-sort themselves).
pub fn iter_builtin_names() -> impl Iterator<Item = &'static str> {
    let installed: Vec<_> = harn_builtin_registry::installed_manifest()
        .into_iter()
        .filter(|entry| {
            matches!(
                entry.contract.exposure,
                BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
            )
        })
        .collect();
    let installed_names: std::collections::HashSet<&'static str> =
        harn_builtin_registry::installed_manifest()
            .into_iter()
            .map(|entry| entry.name)
            .collect();
    installed.into_iter().map(|entry| entry.name).chain(
        signatures::groups()
            .into_iter()
            .flat_map(|g| g.iter())
            .filter(move |s| !installed_names.contains(s.name))
            .map(|s| s.name),
    )
}

/// Names that come *only* from the hand-written static fallback tables
/// (`signatures::groups()`), independent of whatever the driver installed.
///
/// Exposed so cross-crate drift guards (see the builtin-registry alignment
/// test in `harn-vm`) can assert the static tables never overlap with
/// `#[harn_builtin]`-published or `runtime_only` macro builtins — the exact
/// duplication that let LLM config signatures silently drift before the
/// shapes-in-`harn-builtin-meta` migration.
pub fn static_signature_names() -> impl Iterator<Item = &'static str> {
    signatures::groups()
        .into_iter()
        .flat_map(|g| g.iter())
        .map(|s| s.name)
}

/// Iterate over every builtin's name and statically-known return-type
/// strings. Used by `harn-lint` and other consumers that want a
/// lightweight "what does this builtin return" view without bringing in
/// the full type IR.
pub fn iter_builtin_metadata() -> impl Iterator<Item = BuiltinMetadata> {
    let installed: Vec<_> = harn_builtin_registry::installed_manifest()
        .into_iter()
        .filter(|entry| {
            matches!(
                entry.contract.exposure,
                BuiltinExposure::PureGlobal | BuiltinExposure::CapabilityFunction { .. }
            )
        })
        .collect();
    let installed_names: std::collections::HashSet<&'static str> =
        harn_builtin_registry::installed_manifest()
            .into_iter()
            .map(|entry| entry.name)
            .collect();
    installed
        .into_iter()
        .map(|entry| BuiltinMetadata {
            name: entry.name,
            return_types: builtin_return_type_names(entry.signature),
        })
        .chain(
            signatures::groups()
                .into_iter()
                .flat_map(|g| g.iter())
                .filter(move |s| !installed_names.contains(s.name))
                .map(|sig| BuiltinMetadata {
                    name: sig.name,
                    return_types: builtin_return_type_names(sig),
                }),
        )
}

/// Statically-known return type for `name`, materialized as a [`TypeExpr`].
/// Returns `None` for unknown names AND for builtins whose return type is
/// genuinely dynamic ([`Ty::Any`]).
pub fn builtin_return_type(name: &str) -> Option<TypeExpr> {
    let sig = lookup(name)?;
    if sig.returns.is_any() {
        return None;
    }
    Some(sig.returns.to_type_expr())
}

/// Returns true if this builtin produces an untyped/opaque value that
/// should be validated before field access in strict types mode.
///
/// This is the same set the linter's `untyped-dict-access` rule treats
/// as boundary sources — JSON parsing, HTTP responses, LLM outputs,
/// host capability calls, etc.
pub fn is_untyped_boundary_source(name: &str) -> bool {
    matches!(
        name,
        "json_parse"
            | "json_extract"
            | "yaml_parse"
            | "toml_parse"
            | "llm_call"
            | "llm_call_safe"
            | "llm_completion"
            | "http_get"
            | "http_post"
            | "http_put"
            | "http_patch"
            | "http_delete"
            | "http_download"
            | "http_request"
            | "http_session_request"
            | "http_stream_info"
            | "sse_receive"
            | "sse_server_mock_receive"
            | "sse_server_response"
            | "sse_server_status"
            | "websocket_accept"
            | "websocket_receive"
            | "host_call"
            | "connector_call"
            | "host_tool_call"
    )
}

/// Convert the signature's return type to a tiny `&'static [&'static str]`
/// view used by `BuiltinMetadata` consumers (linter, LSP) that don't
/// pull in the full type IR. Only basic primitive names and the common
/// `T | nil` unions are exposed; everything else returns an empty slice
/// so callers know to consult [`builtin_return_type`] instead.
fn builtin_return_type_names(sig: &BuiltinSignature) -> &'static [&'static str] {
    match &sig.returns {
        Ty::Named(name) => match *name {
            "bool" => &["bool"],
            "bytes" => &["bytes"],
            "dict" => &["dict"],
            "float" => &["float"],
            "int" => &["int"],
            "list" => &["list"],
            "nil" => &["nil"],
            "string" => &["string"],
            _ => &[],
        },
        Ty::Union(members) => match *members {
            [Ty::Named("string"), Ty::Named("nil")] => &["string", "nil"],
            [Ty::Named("nil"), Ty::Named("string")] => &["string", "nil"],
            [Ty::Named("int"), Ty::Named("nil")] => &["int", "nil"],
            [Ty::Named("nil"), Ty::Named("int")] => &["int", "nil"],
            [Ty::Named("dict"), Ty::Named("nil")] => &["dict", "nil"],
            [Ty::Named("nil"), Ty::Named("dict")] => &["dict", "nil"],
            [Ty::Named("bytes"), Ty::Named("nil")] => &["bytes", "nil"],
            [Ty::Named("nil"), Ty::Named("bytes")] => &["bytes", "nil"],
            _ => &[],
        },
        Ty::Never => &["never"],
        _ => &[],
    }
}

#[cfg(test)]
mod ambient_install_regression {
    use super::*;

    #[test]
    fn installed_manifest_does_not_shadow_ambient_capability_methods() {
        // Process-global env; restore afterwards so later tests in this
        // process do not inherit the legacy bridge (it previously leaked and
        // made strict-mode typechecker tests order-dependent).
        let previous = std::env::var_os("HARN_LEGACY_AMBIENT_CAPABILITIES");
        std::env::set_var("HARN_LEGACY_AMBIENT_CAPABILITIES", "1");
        crate::refresh_legacy_ambient_capabilities();
        assert!(
            is_builtin("store_set"),
            "capability-contracts fallback must resolve ambient store_set"
        );

        // Project the same contracts the CLI installs before `harn check`.
        let entries: &'static [&'static harn_builtin_registry::BuiltinManifestEntry] = Box::leak(
            harn_capability_contracts::manifest()
                .to_vec()
                .into_boxed_slice(),
        );
        harn_builtin_registry::install_builtin_manifest(entries);

        assert!(
            is_builtin("store_set"),
            "after manifest install, ambient store_set must still resolve uniquely"
        );
        assert!(
            legacy_capability_method_entry("store_set").is_some(),
            "legacy_capability_method_entry must stay unique after install"
        );

        match previous {
            Some(value) => std::env::set_var("HARN_LEGACY_AMBIENT_CAPABILITIES", value),
            None => std::env::remove_var("HARN_LEGACY_AMBIENT_CAPABILITIES"),
        }
        crate::refresh_legacy_ambient_capabilities();
    }
}