Skip to main content

harn_kernel/
type_contract.rs

1//! Runtime type-contract matching shared by native and portable execution.
2//!
3//! The parser owns `TypeExpr`; this module owns how those expressions match
4//! runtime values. Runtimes project their value representation through the
5//! small `TypeContractValue` interface instead of maintaining another type
6//! dispatch table.
7
8use harn_parser::builtin_signatures::{BuiltinSignature, Ty};
9use harn_parser::TypeExpr;
10
11use crate::DataValue;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RuntimeTypeKind {
15    Int,
16    Float,
17    Decimal,
18    String,
19    Bytes,
20    Bool,
21    Nil,
22    List,
23    Dict,
24    Closure,
25    Duration,
26    Enum,
27    Struct,
28    TaskHandle,
29    Channel,
30    Atomic,
31    Rng,
32    SyncPermit,
33    Resource,
34    ResourceGuard,
35    McpClient,
36    VerdictReceipt,
37    Set,
38    Generator,
39    Stream,
40    Range,
41    Iter,
42    Pair,
43    Harness,
44}
45
46pub trait TypeContractValue: Sized {
47    fn runtime_type_kind(&self) -> RuntimeTypeKind;
48    fn list_items(&self) -> Option<&[Self]> {
49        None
50    }
51    fn record_field(&self, _name: &str) -> Option<&Self> {
52        None
53    }
54    /// Apply `predicate` to every value in a dictionary-like value.
55    ///
56    /// Returning `None` distinguishes a value that is not enumerable from an
57    /// empty dictionary. This keeps homogeneous `dict<K, V>` checks exact
58    /// without forcing either runtime to allocate a projection vector.
59    fn record_values_match(&self, _predicate: &mut dyn FnMut(&Self) -> bool) -> Option<bool> {
60        None
61    }
62    fn string_literal(&self) -> Option<&str> {
63        None
64    }
65    fn int_literal(&self) -> Option<i64> {
66        None
67    }
68    fn nominal_type_name(&self) -> Option<&str> {
69        None
70    }
71}
72
73pub fn matches_type<V: TypeContractValue>(
74    value: &V,
75    expected: &TypeExpr,
76    type_params: &[String],
77    nominal_type_names: &[String],
78) -> bool {
79    use RuntimeTypeKind as Kind;
80    match expected {
81        TypeExpr::Named(name) => match name.as_str() {
82            _ if type_params.iter().any(|param| param == name) => true,
83            "any" | "unknown" => true,
84            "int" => value.runtime_type_kind() == Kind::Int,
85            "float" | "number" => matches!(value.runtime_type_kind(), Kind::Float | Kind::Int),
86            "decimal" => value.runtime_type_kind() == Kind::Decimal,
87            "string" => value.runtime_type_kind() == Kind::String,
88            "bool" => value.runtime_type_kind() == Kind::Bool,
89            "nil" => value.runtime_type_kind() == Kind::Nil,
90            "list" => value.runtime_type_kind() == Kind::List,
91            "dict" | "record" => value.runtime_type_kind() == Kind::Dict,
92            "bytes" => value.runtime_type_kind() == Kind::Bytes,
93            "duration" => value.runtime_type_kind() == Kind::Duration,
94            "set" => value.runtime_type_kind() == Kind::Set,
95            "range" => value.runtime_type_kind() == Kind::Range,
96            "iter" => value.runtime_type_kind() == Kind::Iter,
97            "generator" | "Generator" => value.runtime_type_kind() == Kind::Generator,
98            "stream" | "Stream" => value.runtime_type_kind() == Kind::Stream,
99            "channel" => value.runtime_type_kind() == Kind::Channel,
100            "task_handle" => value.runtime_type_kind() == Kind::TaskHandle,
101            "atomic" => value.runtime_type_kind() == Kind::Atomic,
102            "rng" => value.runtime_type_kind() == Kind::Rng,
103            "sync_permit" => value.runtime_type_kind() == Kind::SyncPermit,
104            "resource" => value.runtime_type_kind() == Kind::Resource,
105            "resource_guard" => value.runtime_type_kind() == Kind::ResourceGuard,
106            "mcp_client" => value.runtime_type_kind() == Kind::McpClient,
107            "verdict_receipt" => value.runtime_type_kind() == Kind::VerdictReceipt,
108            "pair" => value.runtime_type_kind() == Kind::Pair,
109            "enum" => value.runtime_type_kind() == Kind::Enum,
110            "struct" => value.runtime_type_kind() == Kind::Struct,
111            "closure" => value.runtime_type_kind() == Kind::Closure,
112            _ if !nominal_type_names.iter().any(|ty| ty == name) => true,
113            _ => value
114                .nominal_type_name()
115                .is_some_and(|actual| actual == name),
116        },
117        TypeExpr::Union(members) => members
118            .iter()
119            .any(|member| matches_type(value, member, type_params, nominal_type_names)),
120        TypeExpr::Intersection(members) => members
121            .iter()
122            .all(|member| matches_type(value, member, type_params, nominal_type_names)),
123        TypeExpr::List(inner) => value.list_items().is_some_and(|items| {
124            items
125                .iter()
126                .all(|item| matches_type(item, inner, type_params, nominal_type_names))
127        }),
128        TypeExpr::Tuple(elements) => value.list_items().is_some_and(|items| {
129            items.len() == elements.len()
130                && items.iter().zip(elements).all(|(item, element)| {
131                    matches_type(item, element, type_params, nominal_type_names)
132                })
133        }),
134        TypeExpr::DictType(_, value_type) => {
135            value.runtime_type_kind() == Kind::Dict
136                && record_values_match(value, value_type, type_params, nominal_type_names)
137        }
138        TypeExpr::Iter(_) | TypeExpr::Generator(_) | TypeExpr::Stream(_) => matches!(
139            value.runtime_type_kind(),
140            Kind::List | Kind::Generator | Kind::Stream
141        ),
142        TypeExpr::Shape(fields) | TypeExpr::OpenShape { fields, .. } => {
143            matches!(value.runtime_type_kind(), Kind::Dict | Kind::Struct)
144                && fields
145                    .iter()
146                    .all(|field| match value.record_field(&field.name) {
147                        Some(field_value)
148                            if field.optional && field_value.runtime_type_kind() == Kind::Nil =>
149                        {
150                            true
151                        }
152                        Some(field_value) => matches_type(
153                            field_value,
154                            &field.type_expr,
155                            type_params,
156                            nominal_type_names,
157                        ),
158                        None => field.optional,
159                    })
160        }
161        TypeExpr::Applied { name, args } => match (name.as_str(), args.as_slice()) {
162            ("list" | "List", [inner]) => value.list_items().is_some_and(|items| {
163                items
164                    .iter()
165                    .all(|item| matches_type(item, inner, type_params, nominal_type_names))
166            }),
167            ("dict" | "Dict", [_, value_type]) => {
168                value.runtime_type_kind() == Kind::Dict
169                    && record_values_match(value, value_type, type_params, nominal_type_names)
170            }
171            ("Option", [inner]) => {
172                value.runtime_type_kind() == Kind::Nil
173                    || matches_type(value, inner, type_params, nominal_type_names)
174            }
175            _ => true,
176        },
177        TypeExpr::FnType { .. } => value.runtime_type_kind() == Kind::Closure,
178        TypeExpr::Never => false,
179        TypeExpr::LitString(expected) => value.string_literal() == Some(expected),
180        TypeExpr::LitInt(expected) => value.int_literal() == Some(*expected),
181        TypeExpr::Owned(inner) => matches_type(value, inner, type_params, nominal_type_names),
182    }
183}
184
185/// Match a value against the canonical const-friendly type used by builtin
186/// and capability manifests.
187///
188/// Conversion is owned by `harn-parser`; runtimes therefore do not maintain a
189/// second interpretation of `Ty` alongside the source-language `TypeExpr`.
190pub fn matches_manifest_type<V: TypeContractValue>(value: &V, expected: &Ty) -> bool {
191    let expected = harn_parser::builtin_signatures::ty_to_type_expr(expected);
192    matches_type(value, &expected, &[], &[])
193}
194
195/// Match a runtime value against the closed schema subset emitted by the
196/// canonical compiler for typed default parameters.
197///
198/// This is intentionally narrower than Harn's user-facing schema language:
199/// it owns only the compiler-generated `type`, `properties`, `required`,
200/// `items`, `additional_properties`, `union`, `all_of`, `enum`, and `const`
201/// vocabulary. Both native and portable runtimes can project values through
202/// [`TypeContractValue`] without growing a second source-type dispatcher.
203pub fn matches_compiler_schema<V: TypeContractValue>(value: &V, schema: &DataValue) -> bool {
204    matches_compiler_schema_inner(value, schema, 0)
205}
206
207fn matches_compiler_schema_inner<V: TypeContractValue>(
208    value: &V,
209    schema: &DataValue,
210    depth: usize,
211) -> bool {
212    const MAX_SCHEMA_DEPTH: usize = 256;
213    if depth >= MAX_SCHEMA_DEPTH {
214        return false;
215    }
216    let DataValue::Record(fields) = schema else {
217        return false;
218    };
219
220    if let Some(DataValue::List(branches)) = fields.get("union") {
221        return branches
222            .iter()
223            .any(|branch| matches_compiler_schema_inner(value, branch, depth + 1));
224    }
225    if let Some(DataValue::List(branches)) = fields.get("all_of") {
226        return branches
227            .iter()
228            .all(|branch| matches_compiler_schema_inner(value, branch, depth + 1));
229    }
230    if let Some(DataValue::String(expected)) = fields.get("type") {
231        use RuntimeTypeKind as Kind;
232        let matches = match expected.as_str() {
233            "int" => value.runtime_type_kind() == Kind::Int,
234            "float" => matches!(value.runtime_type_kind(), Kind::Int | Kind::Float),
235            "string" => value.runtime_type_kind() == Kind::String,
236            "bool" => value.runtime_type_kind() == Kind::Bool,
237            "nil" => value.runtime_type_kind() == Kind::Nil,
238            "list" => value.runtime_type_kind() == Kind::List,
239            "dict" => value.runtime_type_kind() == Kind::Dict,
240            "bytes" => value.runtime_type_kind() == Kind::Bytes,
241            "closure" => value.runtime_type_kind() == Kind::Closure,
242            // Sets cannot cross the portable data boundary.
243            "set" => value.runtime_type_kind() == Kind::Set,
244            _ => false,
245        };
246        if !matches {
247            return false;
248        }
249    }
250    if let Some(DataValue::List(allowed)) = fields.get("enum") {
251        if !allowed
252            .iter()
253            .any(|candidate| literal_matches(value, candidate))
254        {
255            return false;
256        }
257    }
258    if let Some(expected) = fields.get("const") {
259        if !literal_matches(value, expected) {
260            return false;
261        }
262    }
263    if let Some(DataValue::List(required)) = fields.get("required") {
264        if !required.iter().all(|name| match name {
265            DataValue::String(name) => value.record_field(name).is_some(),
266            _ => false,
267        }) {
268            return false;
269        }
270    }
271    if let Some(DataValue::Record(properties)) = fields.get("properties") {
272        for (name, child_schema) in properties {
273            if let Some(child) = value.record_field(name) {
274                if !matches_compiler_schema_inner(child, child_schema, depth + 1) {
275                    return false;
276                }
277            }
278        }
279    }
280    if let Some(additional) = fields.get("additional_properties") {
281        if value.record_values_match(&mut |child| {
282            matches_compiler_schema_inner(child, additional, depth + 1)
283        }) != Some(true)
284        {
285            return false;
286        }
287    }
288    true
289}
290
291fn literal_matches<V: TypeContractValue>(value: &V, expected: &DataValue) -> bool {
292    match expected {
293        DataValue::String(expected) => value.string_literal() == Some(expected),
294        DataValue::Int(expected) => value.int_literal() == Some(*expected),
295        DataValue::Nil => value.runtime_type_kind() == RuntimeTypeKind::Nil,
296        _ => false,
297    }
298}
299
300/// Return whether a canonical manifest type can cross the portable
301/// [`DataValue`] boundary without losing information or type precision.
302///
303/// The capability registry describes both JSON-shaped host calls and native
304/// runtime objects such as channels, streams, closures, schemas, and generic
305/// results. Portable execution must reject the latter structurally instead of
306/// collapsing them to `any` and pretending the contract is enforceable.
307pub fn manifest_type_is_portable(expected: &Ty) -> bool {
308    match expected {
309        Ty::Any | Ty::LitInt(_) | Ty::LitString(_) => true,
310        Ty::Named(name) => matches!(
311            *name,
312            "any"
313                | "unknown"
314                | "nil"
315                | "bool"
316                | "int"
317                | "float"
318                | "number"
319                | "string"
320                | "bytes"
321                | "list"
322                | "dict"
323                | "record"
324        ),
325        Ty::Optional(inner) => manifest_type_is_portable(inner),
326        Ty::Apply("list" | "List", [inner]) | Ty::Apply("Option", [inner]) => {
327            manifest_type_is_portable(inner)
328        }
329        Ty::Apply("dict" | "Dict", [key, value]) => {
330            manifest_dict_key_is_portable(key) && manifest_type_is_portable(value)
331        }
332        Ty::Union(members) => !members.is_empty() && members.iter().all(manifest_type_is_portable),
333        Ty::Shape(fields) => fields
334            .iter()
335            .all(|field| manifest_type_is_portable(&field.ty)),
336        // An open record is portable when its named fields are. The row tails
337        // stand for keys the manifest does not describe, so they are only
338        // portable if they are themselves portable container types.
339        Ty::OpenShape(fields, rests) => {
340            fields
341                .iter()
342                .all(|field| manifest_type_is_portable(&field.ty))
343                && rests.iter().all(manifest_type_is_portable)
344        }
345        Ty::Generic(_) | Ty::Apply(_, _) | Ty::Fn(_, _) | Ty::SchemaOf(_) | Ty::Never => false,
346    }
347}
348
349fn manifest_dict_key_is_portable(expected: &Ty) -> bool {
350    match expected {
351        Ty::Any | Ty::Named("any" | "unknown" | "string") | Ty::LitString(_) => true,
352        Ty::Union(members) => {
353            !members.is_empty() && members.iter().all(manifest_dict_key_is_portable)
354        }
355        _ => false,
356    }
357}
358
359/// Return whether every parameter and successful return value in a canonical
360/// capability signature is representable by the portable value contract.
361pub fn manifest_signature_is_portable(signature: &BuiltinSignature) -> bool {
362    signature
363        .params
364        .iter()
365        .all(|parameter| manifest_type_is_portable(&parameter.ty))
366        && manifest_type_is_portable(&signature.returns)
367}
368
369fn record_values_match<V: TypeContractValue>(
370    value: &V,
371    value_type: &TypeExpr,
372    type_params: &[String],
373    nominal_type_names: &[String],
374) -> bool {
375    value
376        .record_values_match(&mut |field| {
377            matches_type(field, value_type, type_params, nominal_type_names)
378        })
379        .unwrap_or(false)
380}
381
382impl TypeContractValue for DataValue {
383    fn runtime_type_kind(&self) -> RuntimeTypeKind {
384        match self {
385            Self::Nil => RuntimeTypeKind::Nil,
386            Self::Bool(_) => RuntimeTypeKind::Bool,
387            Self::Int(_) => RuntimeTypeKind::Int,
388            Self::Float(_) => RuntimeTypeKind::Float,
389            Self::String(_) => RuntimeTypeKind::String,
390            Self::Bytes(_) => RuntimeTypeKind::Bytes,
391            Self::List(_) => RuntimeTypeKind::List,
392            Self::Record(_) => RuntimeTypeKind::Dict,
393        }
394    }
395
396    fn list_items(&self) -> Option<&[Self]> {
397        match self {
398            Self::List(items) => Some(items),
399            _ => None,
400        }
401    }
402
403    fn record_field(&self, name: &str) -> Option<&Self> {
404        match self {
405            Self::Record(fields) => fields.get(name),
406            _ => None,
407        }
408    }
409
410    fn record_values_match(&self, predicate: &mut dyn FnMut(&Self) -> bool) -> Option<bool> {
411        match self {
412            Self::Record(fields) => Some(fields.values().all(predicate)),
413            _ => None,
414        }
415    }
416
417    fn string_literal(&self) -> Option<&str> {
418        match self {
419            Self::String(value) => Some(value),
420            _ => None,
421        }
422    }
423
424    fn int_literal(&self) -> Option<i64> {
425        match self {
426            Self::Int(value) => Some(*value),
427            _ => None,
428        }
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use harn_parser::builtin_signatures::{ShapeFieldDescriptor, Ty};
435
436    use super::*;
437
438    const STRING: Ty = Ty::Named("string");
439    const STRING_LIST_ARGS: &[Ty] = &[STRING];
440    const RESULT_ARGS: &[Ty] = &[STRING, Ty::Named("dict")];
441    const INT_KEYED_DICT_ARGS: &[Ty] = &[Ty::Named("int"), STRING];
442    const RECORD_FIELDS: &[ShapeFieldDescriptor] = &[
443        ShapeFieldDescriptor::new("name", STRING),
444        ShapeFieldDescriptor::optional("note", STRING),
445    ];
446
447    #[test]
448    fn manifest_types_use_the_source_type_contract() {
449        let strings = DataValue::List(vec![DataValue::String("kernel".into())]);
450        assert!(matches_manifest_type(
451            &strings,
452            &Ty::Apply("list", STRING_LIST_ARGS)
453        ));
454        assert!(!matches_manifest_type(
455            &DataValue::List(vec![DataValue::Int(1)]),
456            &Ty::Apply("list", STRING_LIST_ARGS)
457        ));
458
459        let record = DataValue::Record(std::collections::BTreeMap::from([(
460            "name".to_string(),
461            DataValue::String("portable".into()),
462        )]));
463        assert!(matches_manifest_type(&record, &Ty::Shape(RECORD_FIELDS)));
464    }
465
466    #[test]
467    fn portable_manifest_types_are_exactly_data_value_types() {
468        assert!(manifest_type_is_portable(&Ty::Apply(
469            "list",
470            STRING_LIST_ARGS
471        )));
472        assert!(manifest_type_is_portable(&Ty::Shape(RECORD_FIELDS)));
473        assert!(!manifest_type_is_portable(&Ty::Apply(
474            "Result",
475            RESULT_ARGS
476        )));
477        assert!(!manifest_type_is_portable(&Ty::Named("channel")));
478        assert!(!manifest_type_is_portable(&Ty::Fn(&[], &STRING)));
479        assert!(!manifest_type_is_portable(&Ty::Generic("T")));
480        assert!(!manifest_type_is_portable(&Ty::Apply(
481            "dict",
482            INT_KEYED_DICT_ARGS
483        )));
484    }
485}