1use 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 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
185pub 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
195pub 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 "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
300pub 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 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
359pub fn manifest_signature_is_portable(signature: &BuiltinSignature) -> bool {
362 signature
363 .params
364 .iter()
365 .all(|parameter| manifest_type_is_portable(¶meter.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}