1use harn_kernel::type_contract::{RuntimeTypeKind, TypeContractValue};
27use harn_lexer::Span;
28use harn_parser::builtin_signatures::{self, BuiltinSignature, TyExt};
29use harn_parser::typechecker::format_type;
30use harn_parser::TypeExpr;
31
32use crate::chunk::{BindingTypeSlot, CompiledFunction, ParamSlot};
33use crate::runtime_guards::RuntimeParamGuard;
34use crate::value::{
35 ArgTypeMismatchError, ArityExpect, ArityMismatchError, BindingTypeMismatchError, VmError,
36 VmValue,
37};
38use crate::vm::CallArgs;
39
40impl TypeContractValue for VmValue {
41 fn runtime_type_kind(&self) -> RuntimeTypeKind {
42 match self {
43 Self::Int(_) => RuntimeTypeKind::Int,
44 Self::Float(_) => RuntimeTypeKind::Float,
45 Self::Decimal(_) => RuntimeTypeKind::Decimal,
46 Self::String(_) => RuntimeTypeKind::String,
47 Self::Bytes(_) => RuntimeTypeKind::Bytes,
48 Self::Bool(_) => RuntimeTypeKind::Bool,
49 Self::Nil => RuntimeTypeKind::Nil,
50 Self::List(_) => RuntimeTypeKind::List,
51 Self::Dict(_) => RuntimeTypeKind::Dict,
52 Self::Closure(_) | Self::BuiltinRef(_) | Self::BuiltinRefId(_) => {
53 RuntimeTypeKind::Closure
54 }
55 Self::Duration(_) => RuntimeTypeKind::Duration,
56 Self::EnumVariant(_) => RuntimeTypeKind::Enum,
57 Self::StructInstance(_) => RuntimeTypeKind::Struct,
58 Self::TaskHandle(_) => RuntimeTypeKind::TaskHandle,
59 Self::Channel(_) => RuntimeTypeKind::Channel,
60 Self::Atomic(_) => RuntimeTypeKind::Atomic,
61 Self::Rng(_) => RuntimeTypeKind::Rng,
62 Self::SyncPermit(_) => RuntimeTypeKind::SyncPermit,
63 Self::Resource(_) => RuntimeTypeKind::Resource,
64 Self::ResourceGuard(_) => RuntimeTypeKind::ResourceGuard,
65 Self::McpClient(_) => RuntimeTypeKind::McpClient,
66 Self::VerdictReceipt(_) => RuntimeTypeKind::VerdictReceipt,
67 Self::Set(_) => RuntimeTypeKind::Set,
68 Self::Generator(_) => RuntimeTypeKind::Generator,
69 Self::Stream(_) => RuntimeTypeKind::Stream,
70 Self::Range(_) => RuntimeTypeKind::Range,
71 Self::Iter(_) => RuntimeTypeKind::Iter,
72 Self::Pair(_) => RuntimeTypeKind::Pair,
73 Self::Harness(_) => RuntimeTypeKind::Harness,
74 }
75 }
76
77 fn list_items(&self) -> Option<&[Self]> {
78 match self {
79 Self::List(items) => Some(items),
80 _ => None,
81 }
82 }
83
84 fn record_field(&self, name: &str) -> Option<&Self> {
85 match self {
86 Self::Dict(fields) => fields.get(name),
87 Self::StructInstance(_) => self.struct_field(name),
88 _ => None,
89 }
90 }
91
92 fn record_values_match(&self, predicate: &mut dyn FnMut(&Self) -> bool) -> Option<bool> {
93 match self {
94 Self::Dict(fields) => Some(fields.values().all(predicate)),
95 _ => None,
96 }
97 }
98
99 fn string_literal(&self) -> Option<&str> {
100 match self {
101 Self::String(value) => Some(value.as_str()),
102 _ => None,
103 }
104 }
105
106 fn int_literal(&self) -> Option<i64> {
107 match self {
108 Self::Int(value) => Some(*value),
109 _ => None,
110 }
111 }
112
113 fn nominal_type_name(&self) -> Option<&str> {
114 match self {
115 Self::StructInstance(value) => Some(value.layout.struct_name()),
116 Self::EnumVariant(value) => Some(value.enum_name.as_str()),
117 _ => None,
118 }
119 }
120}
121
122pub fn assert_value_matches_type(
145 value: &VmValue,
146 expected: &TypeExpr,
147 callee: &str,
148 param: &str,
149 span: Option<Span>,
150) -> Result<(), VmError> {
151 assert_value_matches_type_with_generics(value, expected, callee, param, span, &[], &[])
152}
153
154fn assert_value_matches_type_with_generics(
155 value: &VmValue,
156 expected: &TypeExpr,
157 callee: &str,
158 param: &str,
159 span: Option<Span>,
160 type_params: &[String],
161 nominal_type_names: &[String],
162) -> Result<(), VmError> {
163 if matches_type_with_generics(value, expected, type_params, nominal_type_names) {
164 Ok(())
165 } else {
166 Err(VmError::ArgTypeMismatch(Box::new(ArgTypeMismatchError {
167 callee: callee.to_string(),
168 param: param.to_string(),
169 expected: format_type(expected),
170 got: value.type_name(),
171 span,
172 })))
173 }
174}
175
176pub fn validate_binding_type(
185 value: &VmValue,
186 slot: &BindingTypeSlot,
187 span: Option<Span>,
188) -> Result<(), VmError> {
189 if matches_type_with_generics(value, &slot.type_expr, &[], &slot.nominal_type_names) {
190 return Ok(());
191 }
192 Err(VmError::BindingTypeMismatch(Box::new(
193 BindingTypeMismatchError {
194 binding: slot.name.clone(),
195 expected: format_type(&slot.type_expr),
196 got: value.type_name(),
197 span,
198 },
199 )))
200}
201
202fn user_param_for_arg(func: &CompiledFunction, index: usize) -> Option<&ParamSlot> {
203 if func.has_rest_param && index >= func.params.len().saturating_sub(1) {
204 func.params.last()
205 } else {
206 func.params.get(index)
207 }
208}
209
210fn builtin_param_for_arg(
211 sig: &BuiltinSignature,
212 index: usize,
213) -> Option<&harn_parser::builtin_signatures::Param> {
214 if sig.has_rest && index >= sig.params.len().saturating_sub(1) {
215 sig.params.last()
216 } else {
217 sig.params.get(index)
218 }
219}
220
221#[cfg(test)]
224fn matches_type(value: &VmValue, expected: &TypeExpr) -> bool {
225 matches_type_with_generics(value, expected, &[], &[])
226}
227
228fn matches_type_with_generics(
229 value: &VmValue,
230 expected: &TypeExpr,
231 type_params: &[String],
232 nominal_type_names: &[String],
233) -> bool {
234 harn_kernel::type_contract::matches_type(value, expected, type_params, nominal_type_names)
235}
236
237pub fn validate_user_call(
241 func: &CompiledFunction,
242 args: &[VmValue],
243 span: Option<Span>,
244) -> Result<(), VmError> {
245 validate_user_call_args(func, &CallArgs::Slice(args), span)
246}
247
248pub(crate) fn validate_user_call_args(
249 func: &CompiledFunction,
250 args: &CallArgs<'_>,
251 span: Option<Span>,
252) -> Result<(), VmError> {
253 let required = func.minimum_arg_count();
254 let got = args.len();
255
256 if got < required {
257 let expected = arity_expect_for(func);
258 return Err(VmError::ArityMismatch(Box::new(ArityMismatchError {
259 callee: func.name.to_string(),
260 expected,
261 got,
262 span,
263 })));
264 }
265
266 if !func.has_runtime_type_checks {
267 return Ok(());
268 }
269
270 for (i, value) in args.iter().enumerate() {
271 let Some(slot) = user_param_for_arg(func, i) else {
272 continue;
273 };
274 let Some(expected) = &slot.type_expr else {
275 continue;
276 };
277 if let Some(guard) = &slot.runtime_guard {
278 validate_with_runtime_guard(value, guard, func, slot, span)?;
279 continue;
280 }
281 validate_uncached_type_expr(value, expected, func, slot, span)?;
282 }
283
284 Ok(())
285}
286
287fn validate_with_runtime_guard(
288 value: &VmValue,
289 guard: &RuntimeParamGuard,
290 func: &CompiledFunction,
291 slot: &ParamSlot,
292 span: Option<Span>,
293) -> Result<(), VmError> {
294 match guard {
295 RuntimeParamGuard::CanonicalSchema(schema) => {
296 crate::schema::schema_assert_canonical_param(value, &slot.name, schema)
297 .map_err(|error| parameter_error_with_function(error, &func.name))
298 }
299 RuntimeParamGuard::InvalidSchema(error) => Err(VmError::TypeError(format!(
300 "function '{}' parameter '{}': {}",
301 func.name, slot.name, error
302 ))),
303 RuntimeParamGuard::TypeExpr(expected) => {
304 validate_type_expr_without_schema(value, expected, func, slot, span)
305 }
306 }
307}
308
309fn parameter_error_with_function(error: VmError, function_name: &str) -> VmError {
310 match error {
311 VmError::TypeError(message) => {
312 VmError::TypeError(format!("function '{function_name}' {message}"))
313 }
314 VmError::Runtime(message) => VmError::Runtime(message.replacen(
315 "TypeError: ",
316 &format!("TypeError: function '{function_name}' "),
317 1,
318 )),
319 other => other,
320 }
321}
322
323fn validate_uncached_type_expr(
324 value: &VmValue,
325 expected: &TypeExpr,
326 func: &CompiledFunction,
327 slot: &ParamSlot,
328 span: Option<Span>,
329) -> Result<(), VmError> {
330 if matches!(expected, TypeExpr::Named(name) if func.declares_type_param(name)) {
331 return Ok(());
332 }
333 if let Some(schema) = crate::compiler::Compiler::type_expr_to_schema_value(expected) {
334 crate::schema::schema_assert_param(value, &slot.name, &schema)?;
335 return Ok(());
336 }
337 validate_type_expr_without_schema(value, expected, func, slot, span)
338}
339
340fn validate_type_expr_without_schema(
341 value: &VmValue,
342 expected: &TypeExpr,
343 func: &CompiledFunction,
344 slot: &ParamSlot,
345 span: Option<Span>,
346) -> Result<(), VmError> {
347 assert_value_matches_type_with_generics(
348 value,
349 expected,
350 &func.name,
351 &slot.name,
352 span,
353 &func.type_params,
354 &func.nominal_type_names,
355 )
356}
357
358pub fn validate_builtin_call(
365 name: &str,
366 args: &[VmValue],
367 span: Option<Span>,
368) -> Result<(), VmError> {
369 let Some(sig) = builtin_signatures::lookup(name) else {
370 return Ok(());
371 };
372 validate_against_signature(name, sig, args, span)
373}
374
375pub fn validate_against_signature(
379 name: &str,
380 sig: &BuiltinSignature,
381 args: &[VmValue],
382 span: Option<Span>,
383) -> Result<(), VmError> {
384 let total = sig.params.len();
385 let required = sig.required_params();
386 let got = args.len();
387
388 let arity_ok = if sig.has_rest {
389 got >= total.saturating_sub(1)
390 } else {
391 got >= required && got <= total
392 };
393
394 if !arity_ok {
395 let expected = if sig.has_rest {
396 ArityExpect::AtLeast(total.saturating_sub(1))
397 } else if required == total {
398 ArityExpect::Exact(total)
399 } else {
400 ArityExpect::Range {
401 min: required,
402 max: total,
403 }
404 };
405 return Err(VmError::ArityMismatch(Box::new(ArityMismatchError {
406 callee: name.to_string(),
407 expected,
408 got,
409 span,
410 })));
411 }
412
413 for (i, value) in args.iter().enumerate() {
414 let Some(param) = builtin_param_for_arg(sig, i) else {
415 continue;
416 };
417 if param.optional && matches!(value, VmValue::Nil) {
418 continue;
419 }
420 let expected = param.ty.to_type_expr();
425 if matches!(&expected, TypeExpr::Named(n) if sig.is_type_param(n.as_str())) {
426 continue;
427 }
428 if param.ty.is_any() {
431 continue;
432 }
433 if matches!(param.ty, harn_parser::builtin_signatures::Ty::SchemaOf(_)) {
434 continue;
435 }
436 assert_value_matches_type(value, &expected, name, param.name, span)?;
437 }
438
439 Ok(())
440}
441
442fn arity_expect_for(func: &CompiledFunction) -> ArityExpect {
446 ArityExpect::AtLeast(func.minimum_arg_count())
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use crate::chunk::Chunk;
453 use std::sync::Arc;
454
455 fn vm_int(n: i64) -> VmValue {
456 VmValue::Int(n)
457 }
458
459 fn vm_string(s: &str) -> VmValue {
460 VmValue::String(arcstr::ArcStr::from(s))
461 }
462
463 fn vm_dict(entries: impl IntoIterator<Item = (&'static str, VmValue)>) -> VmValue {
464 VmValue::dict(entries)
465 }
466
467 fn ty_int() -> TypeExpr {
468 TypeExpr::Named("int".into())
469 }
470
471 fn ty_string() -> TypeExpr {
472 TypeExpr::Named("string".into())
473 }
474
475 fn param_slot(name: &str, type_expr: Option<TypeExpr>) -> ParamSlot {
476 ParamSlot {
477 name: name.to_string(),
478 runtime_guard: type_expr.as_ref().map(RuntimeParamGuard::from_type_expr),
479 type_expr,
480 has_default: false,
481 }
482 }
483
484 fn compiled_function(params: Vec<ParamSlot>) -> CompiledFunction {
485 let has_runtime_type_checks = CompiledFunction::has_runtime_type_checks_for_params(¶ms);
486 CompiledFunction {
487 name: arcstr::literal!("f"),
488 type_params: Vec::new(),
489 nominal_type_names: Vec::new(),
490 params,
491 default_start: None,
492 chunk: Arc::new(Chunk::new()),
493 is_generator: false,
494 is_stream: false,
495 has_rest_param: false,
496 has_runtime_type_checks,
497 }
498 }
499
500 #[test]
501 fn matches_primitive_types() {
502 assert!(matches_type(&vm_int(42), &ty_int()));
503 assert!(!matches_type(&vm_int(42), &ty_string()));
504 assert!(matches_type(&vm_string("x"), &ty_string()));
505 assert!(matches_type(
506 &VmValue::Bool(true),
507 &TypeExpr::Named("bool".into())
508 ));
509 assert!(matches_type(&VmValue::Nil, &TypeExpr::Named("nil".into())));
510 }
511
512 #[test]
513 fn float_accepts_int_promotion() {
514 assert!(matches_type(&vm_int(3), &TypeExpr::Named("float".into())));
516 assert!(matches_type(
517 &VmValue::Float(3.0),
518 &TypeExpr::Named("float".into())
519 ));
520 }
521
522 #[test]
523 fn union_accepts_any_member() {
524 let union = TypeExpr::Union(vec![ty_int(), ty_string()]);
525 assert!(matches_type(&vm_int(1), &union));
526 assert!(matches_type(&vm_string("y"), &union));
527 assert!(!matches_type(&VmValue::Bool(true), &union));
528 }
529
530 #[test]
531 fn optional_accepts_nil() {
532 let opt = TypeExpr::Union(vec![ty_string(), TypeExpr::Named("nil".into())]);
533 assert!(matches_type(&VmValue::Nil, &opt));
534 assert!(matches_type(&vm_string("x"), &opt));
535 assert!(!matches_type(&vm_int(1), &opt));
536 }
537
538 #[test]
539 fn list_validates_elements() {
540 let list_int = TypeExpr::List(Box::new(ty_int()));
541 let good = VmValue::List(std::sync::Arc::new(vec![vm_int(1), vm_int(2)]));
542 let bad = VmValue::List(std::sync::Arc::new(vec![vm_int(1), vm_string("x")]));
543 assert!(matches_type(&good, &list_int));
544 assert!(!matches_type(&bad, &list_int));
545 }
546
547 #[test]
548 fn tuple_validates_arity_and_each_position() {
549 let tuple = TypeExpr::Tuple(vec![ty_int(), ty_string()]);
550 let good = VmValue::List(std::sync::Arc::new(vec![vm_int(1), vm_string("x")]));
551 let wrong_position = VmValue::List(std::sync::Arc::new(vec![vm_string("x"), vm_int(1)]));
552 let wrong_arity = VmValue::List(std::sync::Arc::new(vec![vm_int(1)]));
553 assert!(matches_type(&good, &tuple));
554 assert!(!matches_type(&wrong_position, &tuple));
555 assert!(!matches_type(&wrong_arity, &tuple));
556 }
557
558 #[test]
559 fn shape_validates_required_fields() {
560 let shape = TypeExpr::Shape(vec![harn_parser::ShapeField::synthetic(
561 "x",
562 ty_int(),
563 false,
564 )]);
565 let mut good = std::collections::BTreeMap::new();
566 good.insert("x".to_string(), vm_int(7));
567 assert!(matches_type(&VmValue::dict(good), &shape));
568 assert!(!matches_type(
569 &VmValue::dict_map(Default::default()),
570 &shape
571 ));
572 }
573
574 #[test]
575 fn named_type_matches_user_struct_name() {
576 let custom = TypeExpr::Named("MyStruct".into());
577 assert!(!matches_type_with_generics(
578 &vm_int(1),
579 &custom,
580 &[],
581 &["MyStruct".to_string()]
582 ));
583 assert!(matches_type_with_generics(
584 &VmValue::struct_instance("MyStruct", Default::default()),
585 &custom,
586 &[],
587 &["MyStruct".to_string()]
588 ));
589 }
590
591 #[test]
592 fn lit_int_requires_value_equality() {
593 assert!(matches_type(&vm_int(42), &TypeExpr::LitInt(42)));
594 assert!(!matches_type(&vm_int(7), &TypeExpr::LitInt(42)));
595 }
596
597 #[test]
598 fn assert_value_returns_arg_type_mismatch_on_fail() {
599 let err =
600 assert_value_matches_type(&vm_string("abc"), &ty_int(), "myFn", "n", None).unwrap_err();
601 match err {
602 VmError::ArgTypeMismatch(err) => {
603 assert_eq!(err.callee, "myFn");
604 assert_eq!(err.param, "n");
605 assert_eq!(err.expected, "int");
606 assert_eq!(err.got, "string");
607 assert!(err.span.is_none());
608 }
609 other => panic!("expected ArgTypeMismatch, got {other:?}"),
610 }
611 }
612
613 #[test]
614 fn validate_user_call_skips_param_walk_for_untyped_function() {
615 let func = compiled_function(vec![param_slot("value", None)]);
616
617 validate_user_call(&func, &[vm_string("anything")], None).unwrap();
618
619 let err = validate_user_call(&func, &[], None).unwrap_err();
620 assert!(matches!(err, VmError::ArityMismatch(_)));
621 }
622
623 #[test]
624 fn validate_user_call_checks_typed_function() {
625 let func = compiled_function(vec![param_slot("value", Some(ty_int()))]);
626
627 validate_user_call(&func, &[vm_int(1)], None).unwrap();
628
629 let err = validate_user_call(&func, &[vm_string("bad")], None).unwrap_err();
630 assert!(matches!(err, VmError::Runtime(_) | VmError::TypeError(_)));
631 }
632
633 #[test]
634 fn validate_user_call_uses_cached_runtime_guard_metadata() {
635 let string_schema = VmValue::dict(std::collections::BTreeMap::from([(
636 "type".to_string(),
637 VmValue::String(arcstr::ArcStr::from("string")),
638 )]));
639 let guard = RuntimeParamGuard::CanonicalSchema(
640 crate::schema::canonical_param_schema(&string_schema).unwrap(),
641 );
642 let func = compiled_function(vec![ParamSlot {
643 name: "value".to_string(),
644 type_expr: Some(ty_int()),
645 runtime_guard: Some(guard),
646 has_default: false,
647 }]);
648
649 validate_user_call(&func, &[vm_string("cached")], None).unwrap();
650 validate_user_call(&func, &[vm_string("guard")], None).unwrap();
651
652 let err = validate_user_call(&func, &[vm_int(1)], None).unwrap_err();
653 assert!(matches!(err, VmError::Runtime(_) | VmError::TypeError(_)));
654 }
655
656 #[test]
657 fn runtime_guard_does_not_narrow_partially_lowerable_union() {
658 let func = compiled_function(vec![param_slot(
659 "options",
660 Some(TypeExpr::Union(vec![
661 TypeExpr::Named("OpenOptions".into()),
662 TypeExpr::Named("nil".into()),
663 ])),
664 )]);
665
666 validate_user_call(&func, &[VmValue::Nil], None).unwrap();
667 validate_user_call(&func, &[vm_dict([("foo", vm_string("ok"))])], None).unwrap();
668 }
669}