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/// Return whether a canonical manifest type can cross the portable
196/// [`DataValue`] boundary without losing information or type precision.
197///
198/// The capability registry describes both JSON-shaped host calls and native
199/// runtime objects such as channels, streams, closures, schemas, and generic
200/// results. Portable execution must reject the latter structurally instead of
201/// collapsing them to `any` and pretending the contract is enforceable.
202pub fn manifest_type_is_portable(expected: &Ty) -> bool {
203    match expected {
204        Ty::Any | Ty::LitInt(_) | Ty::LitString(_) => true,
205        Ty::Named(name) => matches!(
206            *name,
207            "any"
208                | "unknown"
209                | "nil"
210                | "bool"
211                | "int"
212                | "float"
213                | "number"
214                | "string"
215                | "bytes"
216                | "list"
217                | "dict"
218                | "record"
219        ),
220        Ty::Optional(inner) => manifest_type_is_portable(inner),
221        Ty::Apply("list" | "List", [inner]) | Ty::Apply("Option", [inner]) => {
222            manifest_type_is_portable(inner)
223        }
224        Ty::Apply("dict" | "Dict", [key, value]) => {
225            manifest_dict_key_is_portable(key) && manifest_type_is_portable(value)
226        }
227        Ty::Union(members) => !members.is_empty() && members.iter().all(manifest_type_is_portable),
228        Ty::Shape(fields) => fields
229            .iter()
230            .all(|field| manifest_type_is_portable(&field.ty)),
231        Ty::Generic(_) | Ty::Apply(_, _) | Ty::Fn(_, _) | Ty::SchemaOf(_) | Ty::Never => false,
232    }
233}
234
235fn manifest_dict_key_is_portable(expected: &Ty) -> bool {
236    match expected {
237        Ty::Any | Ty::Named("any" | "unknown" | "string") | Ty::LitString(_) => true,
238        Ty::Union(members) => {
239            !members.is_empty() && members.iter().all(manifest_dict_key_is_portable)
240        }
241        _ => false,
242    }
243}
244
245/// Return whether every parameter and successful return value in a canonical
246/// capability signature is representable by the portable value contract.
247pub fn manifest_signature_is_portable(signature: &BuiltinSignature) -> bool {
248    signature
249        .params
250        .iter()
251        .all(|parameter| manifest_type_is_portable(&parameter.ty))
252        && manifest_type_is_portable(&signature.returns)
253}
254
255fn record_values_match<V: TypeContractValue>(
256    value: &V,
257    value_type: &TypeExpr,
258    type_params: &[String],
259    nominal_type_names: &[String],
260) -> bool {
261    value
262        .record_values_match(&mut |field| {
263            matches_type(field, value_type, type_params, nominal_type_names)
264        })
265        .unwrap_or(false)
266}
267
268impl TypeContractValue for DataValue {
269    fn runtime_type_kind(&self) -> RuntimeTypeKind {
270        match self {
271            Self::Nil => RuntimeTypeKind::Nil,
272            Self::Bool(_) => RuntimeTypeKind::Bool,
273            Self::Int(_) => RuntimeTypeKind::Int,
274            Self::Float(_) => RuntimeTypeKind::Float,
275            Self::String(_) => RuntimeTypeKind::String,
276            Self::Bytes(_) => RuntimeTypeKind::Bytes,
277            Self::List(_) => RuntimeTypeKind::List,
278            Self::Record(_) => RuntimeTypeKind::Dict,
279        }
280    }
281
282    fn list_items(&self) -> Option<&[Self]> {
283        match self {
284            Self::List(items) => Some(items),
285            _ => None,
286        }
287    }
288
289    fn record_field(&self, name: &str) -> Option<&Self> {
290        match self {
291            Self::Record(fields) => fields.get(name),
292            _ => None,
293        }
294    }
295
296    fn record_values_match(&self, predicate: &mut dyn FnMut(&Self) -> bool) -> Option<bool> {
297        match self {
298            Self::Record(fields) => Some(fields.values().all(predicate)),
299            _ => None,
300        }
301    }
302
303    fn string_literal(&self) -> Option<&str> {
304        match self {
305            Self::String(value) => Some(value),
306            _ => None,
307        }
308    }
309
310    fn int_literal(&self) -> Option<i64> {
311        match self {
312            Self::Int(value) => Some(*value),
313            _ => None,
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use harn_parser::builtin_signatures::{ShapeFieldDescriptor, Ty};
321
322    use super::*;
323
324    const STRING: Ty = Ty::Named("string");
325    const STRING_LIST_ARGS: &[Ty] = &[STRING];
326    const RESULT_ARGS: &[Ty] = &[STRING, Ty::Named("dict")];
327    const INT_KEYED_DICT_ARGS: &[Ty] = &[Ty::Named("int"), STRING];
328    const RECORD_FIELDS: &[ShapeFieldDescriptor] = &[
329        ShapeFieldDescriptor::new("name", STRING),
330        ShapeFieldDescriptor::optional("note", STRING),
331    ];
332
333    #[test]
334    fn manifest_types_use_the_source_type_contract() {
335        let strings = DataValue::List(vec![DataValue::String("kernel".into())]);
336        assert!(matches_manifest_type(
337            &strings,
338            &Ty::Apply("list", STRING_LIST_ARGS)
339        ));
340        assert!(!matches_manifest_type(
341            &DataValue::List(vec![DataValue::Int(1)]),
342            &Ty::Apply("list", STRING_LIST_ARGS)
343        ));
344
345        let record = DataValue::Record(std::collections::BTreeMap::from([(
346            "name".to_string(),
347            DataValue::String("portable".into()),
348        )]));
349        assert!(matches_manifest_type(&record, &Ty::Shape(RECORD_FIELDS)));
350    }
351
352    #[test]
353    fn portable_manifest_types_are_exactly_data_value_types() {
354        assert!(manifest_type_is_portable(&Ty::Apply(
355            "list",
356            STRING_LIST_ARGS
357        )));
358        assert!(manifest_type_is_portable(&Ty::Shape(RECORD_FIELDS)));
359        assert!(!manifest_type_is_portable(&Ty::Apply(
360            "Result",
361            RESULT_ARGS
362        )));
363        assert!(!manifest_type_is_portable(&Ty::Named("channel")));
364        assert!(!manifest_type_is_portable(&Ty::Fn(&[], &STRING)));
365        assert!(!manifest_type_is_portable(&Ty::Generic("T")));
366        assert!(!manifest_type_is_portable(&Ty::Apply(
367            "dict",
368            INT_KEYED_DICT_ARGS
369        )));
370    }
371}