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