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