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
73fn is_nominal(name: &str, nominal_type_names: &[String]) -> bool {
74 nominal_type_names.iter().any(|ty| ty == name)
75}
76
77pub fn matches_type<V: TypeContractValue>(
78 value: &V,
79 expected: &TypeExpr,
80 type_params: &[String],
81 nominal_type_names: &[String],
82) -> bool {
83 use RuntimeTypeKind as Kind;
84 match expected {
85 TypeExpr::Named(name) => match name.as_str() {
86 _ if type_params.iter().any(|param| param == name) => true,
87 "any" | "unknown" => true,
88 "int" => value.runtime_type_kind() == Kind::Int,
89 "float" | "number" => matches!(value.runtime_type_kind(), Kind::Float | Kind::Int),
90 "decimal" => value.runtime_type_kind() == Kind::Decimal,
91 "string" => value.runtime_type_kind() == Kind::String,
92 "bool" => value.runtime_type_kind() == Kind::Bool,
93 "nil" => value.runtime_type_kind() == Kind::Nil,
94 "list" => value.runtime_type_kind() == Kind::List,
95 "dict" | "record" => value.runtime_type_kind() == Kind::Dict,
96 "bytes" => value.runtime_type_kind() == Kind::Bytes,
97 "duration" => value.runtime_type_kind() == Kind::Duration,
98 "set" => value.runtime_type_kind() == Kind::Set,
99 "range" => value.runtime_type_kind() == Kind::Range,
100 "iter" => value.runtime_type_kind() == Kind::Iter,
101 "generator" | "Generator" => value.runtime_type_kind() == Kind::Generator,
102 "stream" | "Stream" => value.runtime_type_kind() == Kind::Stream,
103 "channel" => value.runtime_type_kind() == Kind::Channel,
104 "task_handle" => value.runtime_type_kind() == Kind::TaskHandle,
105 "atomic" => value.runtime_type_kind() == Kind::Atomic,
106 "rng" => value.runtime_type_kind() == Kind::Rng,
107 "sync_permit" => value.runtime_type_kind() == Kind::SyncPermit,
108 "resource" => value.runtime_type_kind() == Kind::Resource,
109 "resource_guard" => value.runtime_type_kind() == Kind::ResourceGuard,
110 "mcp_client" => value.runtime_type_kind() == Kind::McpClient,
111 "verdict_receipt" => value.runtime_type_kind() == Kind::VerdictReceipt,
112 "pair" => value.runtime_type_kind() == Kind::Pair,
113 "enum" => value.runtime_type_kind() == Kind::Enum,
114 "struct" => value.runtime_type_kind() == Kind::Struct,
115 "closure" => value.runtime_type_kind() == Kind::Closure,
116 _ if !nominal_type_names.iter().any(|ty| ty == name) => true,
117 _ => value
118 .nominal_type_name()
119 .is_some_and(|actual| actual == name),
120 },
121 TypeExpr::Union(members) => members
122 .iter()
123 .any(|member| matches_type(value, member, type_params, nominal_type_names)),
124 TypeExpr::Intersection(members) => members
125 .iter()
126 .all(|member| matches_type(value, member, type_params, nominal_type_names)),
127 TypeExpr::List(inner) => value.list_items().is_some_and(|items| {
128 items
129 .iter()
130 .all(|item| matches_type(item, inner, type_params, nominal_type_names))
131 }),
132 TypeExpr::Tuple(elements) => value.list_items().is_some_and(|items| {
133 items.len() == elements.len()
134 && items.iter().zip(elements).all(|(item, element)| {
135 matches_type(item, element, type_params, nominal_type_names)
136 })
137 }),
138 TypeExpr::DictType(_, value_type) => {
139 value.runtime_type_kind() == Kind::Dict
140 && record_values_match(value, value_type, type_params, nominal_type_names)
141 }
142 TypeExpr::Iter(_) | TypeExpr::Generator(_) | TypeExpr::Stream(_) => matches!(
143 value.runtime_type_kind(),
144 Kind::List | Kind::Generator | Kind::Stream
145 ),
146 TypeExpr::Shape(fields) | TypeExpr::OpenShape { fields, .. } => {
147 matches!(value.runtime_type_kind(), Kind::Dict | Kind::Struct)
148 && fields
149 .iter()
150 .all(|field| match value.record_field(&field.name) {
151 Some(field_value)
152 if field.optional && field_value.runtime_type_kind() == Kind::Nil =>
153 {
154 true
155 }
156 Some(field_value) => matches_type(
157 field_value,
158 &field.type_expr,
159 type_params,
160 nominal_type_names,
161 ),
162 None => field.optional,
163 })
164 }
165 TypeExpr::Applied { name, args } => match (name.as_str(), args.as_slice()) {
166 ("list" | "List", [inner]) => value.list_items().is_some_and(|items| {
167 items
168 .iter()
169 .all(|item| matches_type(item, inner, type_params, nominal_type_names))
170 }),
171 ("dict" | "Dict", [_, value_type]) => {
172 value.runtime_type_kind() == Kind::Dict
173 && record_values_match(value, value_type, type_params, nominal_type_names)
174 }
175 ("Option", [inner]) if !is_nominal("Option", nominal_type_names) => {
180 value.runtime_type_kind() == Kind::Nil
181 || matches_type(value, inner, type_params, nominal_type_names)
182 }
183 (name, _) if is_nominal(name, nominal_type_names) => value
187 .nominal_type_name()
188 .is_some_and(|actual| actual == name),
189 _ => true,
190 },
191 TypeExpr::FnType { .. } => value.runtime_type_kind() == Kind::Closure,
192 TypeExpr::Never => false,
193 TypeExpr::LitString(expected) => value.string_literal() == Some(expected),
194 TypeExpr::LitInt(expected) => value.int_literal() == Some(*expected),
195 TypeExpr::Owned(inner) => matches_type(value, inner, type_params, nominal_type_names),
196 }
197}
198
199pub fn matches_manifest_type<V: TypeContractValue>(value: &V, expected: &Ty) -> bool {
205 let expected = harn_parser::builtin_signatures::ty_to_type_expr(expected);
206 matches_type(value, &expected, &[], &[])
207}
208
209pub fn matches_compiler_schema<V: TypeContractValue>(value: &V, schema: &DataValue) -> bool {
218 matches_compiler_schema_inner(value, schema, 0)
219}
220
221fn matches_compiler_schema_inner<V: TypeContractValue>(
222 value: &V,
223 schema: &DataValue,
224 depth: usize,
225) -> bool {
226 const MAX_SCHEMA_DEPTH: usize = 256;
227 if depth >= MAX_SCHEMA_DEPTH {
228 return false;
229 }
230 let DataValue::Record(fields) = schema else {
231 return false;
232 };
233
234 if let Some(DataValue::List(branches)) = fields.get("union") {
235 return branches
236 .iter()
237 .any(|branch| matches_compiler_schema_inner(value, branch, depth + 1));
238 }
239 if let Some(DataValue::List(branches)) = fields.get("all_of") {
240 return branches
241 .iter()
242 .all(|branch| matches_compiler_schema_inner(value, branch, depth + 1));
243 }
244 if let Some(DataValue::String(expected)) = fields.get("type") {
245 use RuntimeTypeKind as Kind;
246 let matches = match expected.as_str() {
247 "int" => value.runtime_type_kind() == Kind::Int,
248 "float" => matches!(value.runtime_type_kind(), Kind::Int | Kind::Float),
249 "string" => value.runtime_type_kind() == Kind::String,
250 "bool" => value.runtime_type_kind() == Kind::Bool,
251 "nil" => value.runtime_type_kind() == Kind::Nil,
252 "list" => value.runtime_type_kind() == Kind::List,
253 "dict" => value.runtime_type_kind() == Kind::Dict,
254 "bytes" => value.runtime_type_kind() == Kind::Bytes,
255 "closure" => value.runtime_type_kind() == Kind::Closure,
256 "set" => value.runtime_type_kind() == Kind::Set,
258 _ => false,
259 };
260 if !matches {
261 return false;
262 }
263 }
264 if let Some(DataValue::List(allowed)) = fields.get("enum") {
265 if !allowed
266 .iter()
267 .any(|candidate| literal_matches(value, candidate))
268 {
269 return false;
270 }
271 }
272 if let Some(expected) = fields.get("const") {
273 if !literal_matches(value, expected) {
274 return false;
275 }
276 }
277 if let Some(DataValue::List(required)) = fields.get("required") {
278 if !required.iter().all(|name| match name {
279 DataValue::String(name) => value.record_field(name).is_some(),
280 _ => false,
281 }) {
282 return false;
283 }
284 }
285 if let Some(DataValue::Record(properties)) = fields.get("properties") {
286 for (name, child_schema) in properties {
287 if let Some(child) = value.record_field(name) {
288 if !matches_compiler_schema_inner(child, child_schema, depth + 1) {
289 return false;
290 }
291 }
292 }
293 }
294 if let Some(additional) = fields.get("additional_properties") {
295 if value.record_values_match(&mut |child| {
296 matches_compiler_schema_inner(child, additional, depth + 1)
297 }) != Some(true)
298 {
299 return false;
300 }
301 }
302 true
303}
304
305fn literal_matches<V: TypeContractValue>(value: &V, expected: &DataValue) -> bool {
306 match expected {
307 DataValue::String(expected) => value.string_literal() == Some(expected),
308 DataValue::Int(expected) => value.int_literal() == Some(*expected),
309 DataValue::Nil => value.runtime_type_kind() == RuntimeTypeKind::Nil,
310 _ => false,
311 }
312}
313
314pub fn manifest_type_is_portable(expected: &Ty) -> bool {
322 match expected {
323 Ty::Any | Ty::LitInt(_) | Ty::LitString(_) => true,
324 Ty::Named(name) => matches!(
325 *name,
326 "any"
327 | "unknown"
328 | "nil"
329 | "bool"
330 | "int"
331 | "float"
332 | "number"
333 | "string"
334 | "bytes"
335 | "list"
336 | "dict"
337 | "record"
338 ),
339 Ty::Optional(inner) => manifest_type_is_portable(inner),
340 Ty::Apply("list" | "List", [inner]) | Ty::Apply("Option", [inner]) => {
341 manifest_type_is_portable(inner)
342 }
343 Ty::Apply("dict" | "Dict", [key, value]) => {
344 manifest_dict_key_is_portable(key) && manifest_type_is_portable(value)
345 }
346 Ty::Union(members) => !members.is_empty() && members.iter().all(manifest_type_is_portable),
347 Ty::Shape(fields) => fields
348 .iter()
349 .all(|field| manifest_type_is_portable(&field.ty)),
350 Ty::OpenShape(fields, rests) => {
354 fields
355 .iter()
356 .all(|field| manifest_type_is_portable(&field.ty))
357 && rests.iter().all(manifest_type_is_portable)
358 }
359 Ty::Generic(_) | Ty::Apply(_, _) | Ty::Fn(_, _) | Ty::SchemaOf(_) | Ty::Never => false,
360 }
361}
362
363fn manifest_dict_key_is_portable(expected: &Ty) -> bool {
364 match expected {
365 Ty::Any | Ty::Named("any" | "unknown" | "string") | Ty::LitString(_) => true,
366 Ty::Union(members) => {
367 !members.is_empty() && members.iter().all(manifest_dict_key_is_portable)
368 }
369 _ => false,
370 }
371}
372
373pub fn manifest_signature_is_portable(signature: &BuiltinSignature) -> bool {
376 signature
377 .params
378 .iter()
379 .all(|parameter| manifest_type_is_portable(¶meter.ty))
380 && manifest_type_is_portable(&signature.returns)
381}
382
383fn record_values_match<V: TypeContractValue>(
384 value: &V,
385 value_type: &TypeExpr,
386 type_params: &[String],
387 nominal_type_names: &[String],
388) -> bool {
389 value
390 .record_values_match(&mut |field| {
391 matches_type(field, value_type, type_params, nominal_type_names)
392 })
393 .unwrap_or(false)
394}
395
396impl TypeContractValue for DataValue {
397 fn runtime_type_kind(&self) -> RuntimeTypeKind {
398 match self {
399 Self::Nil => RuntimeTypeKind::Nil,
400 Self::Bool(_) => RuntimeTypeKind::Bool,
401 Self::Int(_) => RuntimeTypeKind::Int,
402 Self::Float(_) => RuntimeTypeKind::Float,
403 Self::String(_) => RuntimeTypeKind::String,
404 Self::Bytes(_) => RuntimeTypeKind::Bytes,
405 Self::List(_) => RuntimeTypeKind::List,
406 Self::Record(_) => RuntimeTypeKind::Dict,
407 }
408 }
409
410 fn list_items(&self) -> Option<&[Self]> {
411 match self {
412 Self::List(items) => Some(items),
413 _ => None,
414 }
415 }
416
417 fn record_field(&self, name: &str) -> Option<&Self> {
418 match self {
419 Self::Record(fields) => fields.get(name),
420 _ => None,
421 }
422 }
423
424 fn record_values_match(&self, predicate: &mut dyn FnMut(&Self) -> bool) -> Option<bool> {
425 match self {
426 Self::Record(fields) => Some(fields.values().all(predicate)),
427 _ => None,
428 }
429 }
430
431 fn string_literal(&self) -> Option<&str> {
432 match self {
433 Self::String(value) => Some(value),
434 _ => None,
435 }
436 }
437
438 fn int_literal(&self) -> Option<i64> {
439 match self {
440 Self::Int(value) => Some(*value),
441 _ => None,
442 }
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use harn_parser::builtin_signatures::{ShapeFieldDescriptor, Ty};
449
450 use super::*;
451
452 const STRING: Ty = Ty::Named("string");
453 const STRING_LIST_ARGS: &[Ty] = &[STRING];
454 const RESULT_ARGS: &[Ty] = &[STRING, Ty::Named("dict")];
455 const INT_KEYED_DICT_ARGS: &[Ty] = &[Ty::Named("int"), STRING];
456 const RECORD_FIELDS: &[ShapeFieldDescriptor] = &[
457 ShapeFieldDescriptor::new("name", STRING),
458 ShapeFieldDescriptor::optional("note", STRING),
459 ];
460
461 #[test]
462 fn manifest_types_use_the_source_type_contract() {
463 let strings = DataValue::List(vec![DataValue::String("kernel".into())]);
464 assert!(matches_manifest_type(
465 &strings,
466 &Ty::Apply("list", STRING_LIST_ARGS)
467 ));
468 assert!(!matches_manifest_type(
469 &DataValue::List(vec![DataValue::Int(1)]),
470 &Ty::Apply("list", STRING_LIST_ARGS)
471 ));
472
473 let record = DataValue::Record(std::collections::BTreeMap::from([(
474 "name".to_string(),
475 DataValue::String("portable".into()),
476 )]));
477 assert!(matches_manifest_type(&record, &Ty::Shape(RECORD_FIELDS)));
478 }
479
480 #[test]
481 fn portable_manifest_types_are_exactly_data_value_types() {
482 assert!(manifest_type_is_portable(&Ty::Apply(
483 "list",
484 STRING_LIST_ARGS
485 )));
486 assert!(manifest_type_is_portable(&Ty::Shape(RECORD_FIELDS)));
487 assert!(!manifest_type_is_portable(&Ty::Apply(
488 "Result",
489 RESULT_ARGS
490 )));
491 assert!(!manifest_type_is_portable(&Ty::Named("channel")));
492 assert!(!manifest_type_is_portable(&Ty::Fn(&[], &STRING)));
493 assert!(!manifest_type_is_portable(&Ty::Generic("T")));
494 assert!(!manifest_type_is_portable(&Ty::Apply(
495 "dict",
496 INT_KEYED_DICT_ARGS
497 )));
498 }
499
500 struct Nominal(&'static str);
504
505 impl TypeContractValue for Nominal {
506 fn runtime_type_kind(&self) -> RuntimeTypeKind {
507 RuntimeTypeKind::Enum
508 }
509
510 fn nominal_type_name(&self) -> Option<&str> {
511 Some(self.0)
512 }
513 }
514
515 #[test]
520 fn a_user_declared_option_is_matched_nominally() {
521 let applied = TypeExpr::Applied {
522 name: "Option".to_string(),
523 args: vec![TypeExpr::Named("int".to_string())],
524 };
525 let user_declared = vec!["Option".to_string()];
526
527 assert!(
528 matches_type(&Nominal("Option"), &applied, &[], &user_declared),
529 "a user-declared Option enum must satisfy its own type"
530 );
531 assert!(
532 !matches_type(&Nominal("Result"), &applied, &[], &user_declared),
533 "and must still reject a different nominal type"
534 );
535
536 assert!(matches_type(&DataValue::Nil, &applied, &[], &[]));
539 assert!(matches_type(&DataValue::Int(1), &applied, &[], &[]));
540 assert!(!matches_type(
541 &DataValue::String("no".into()),
542 &applied,
543 &[],
544 &[]
545 ));
546 }
547}