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