1use std::collections::HashSet;
22
23struct RAssertion {
28 message: String,
30 condition: String,
32}
33
34impl RAssertion {
35 fn new(message: impl Into<String>, condition: impl Into<String>) -> Self {
37 Self {
38 message: message.into(),
39 condition: condition.into(),
40 }
41 }
42
43 fn to_stopifnot_arg(&self) -> String {
45 format!("\"{}\" = {}", self.message, self.condition)
46 }
47
48 fn nullable(self, param: &str) -> Self {
51 let message = if self.message.contains("must be ") {
52 self.message.replacen("must be ", "must be NULL or ", 1)
54 } else if self.message.contains("must have ") {
55 self.message
57 .replacen("must have ", "must be NULL or have ", 1)
58 } else {
59 format!("{} (or NULL)", self.message)
60 };
61 Self {
62 message,
63 condition: format!("is.null({}) || {}", param, self.condition),
64 }
65 }
66}
67
68#[derive(Clone, Default)]
84pub struct PreconditionOptions {
85 pub coerce_all: bool,
87 pub coerce_params: HashSet<String>,
89}
90
91impl PreconditionOptions {
92 fn is_coerced(&self, r_name: &str) -> bool {
95 self.coerce_all || self.coerce_params.contains(r_name)
96 }
97}
98
99enum RTypeCheck {
107 ScalarNumeric,
110 ScalarNonNeg,
113 Scalar(&'static str),
116 VectorNumeric,
120 VectorIntegerStrict,
129 VectorIntegerWide,
140 Vector(&'static str),
143 Nullable(Box<RTypeCheck>),
146 List,
149}
150
151fn numeric_type_check(param: &str) -> String {
157 format!(
158 "is.numeric({p}) || is.logical({p}) || is.raw({p})",
159 p = param
160 )
161}
162
163fn integer_vector_wide_check(param: &str) -> String {
172 format!(
173 "is.integer({p}) || is.logical({p}) || is.raw({p}) || \
174 (is.numeric({p}) && all(is.na({p}) | {p} == trunc({p})))",
175 p = param
176 )
177}
178
179impl RTypeCheck {
180 fn assertions(&self, param: &str) -> Vec<RAssertion> {
186 match self {
187 RTypeCheck::ScalarNumeric => vec![
188 RAssertion::new(
189 format!("'{}' must be numeric, logical, or raw", param),
190 numeric_type_check(param),
191 ),
192 RAssertion::new(
193 format!("'{}' must have length 1", param),
194 format!("length({}) == 1L", param),
195 ),
196 ],
197 RTypeCheck::ScalarNonNeg => vec![
198 RAssertion::new(
199 format!("'{}' must be numeric, logical, or raw", param),
200 numeric_type_check(param),
201 ),
202 RAssertion::new(
203 format!("'{}' must have length 1", param),
204 format!("length({}) == 1L", param),
205 ),
206 RAssertion::new(
207 format!("'{}' must be non-negative", param),
208 format!("is.raw({p}) || {p} >= 0", p = param),
211 ),
212 ],
213 RTypeCheck::Scalar(r_type) => vec![
214 RAssertion::new(
215 format!("'{}' must be {}", param, r_type),
216 format!("is.{}({})", r_type, param),
217 ),
218 RAssertion::new(
219 format!("'{}' must have length 1", param),
220 format!("length({}) == 1L", param),
221 ),
222 ],
223 RTypeCheck::VectorNumeric => vec![RAssertion::new(
224 format!("'{}' must be numeric, logical, or raw", param),
225 numeric_type_check(param),
226 )],
227 RTypeCheck::VectorIntegerStrict => vec![RAssertion::new(
228 format!("'{}' must be an integer vector", param),
229 format!("is.integer({})", param),
230 )],
231 RTypeCheck::VectorIntegerWide => vec![RAssertion::new(
232 format!("'{}' must be integer or whole-number numeric", param),
233 integer_vector_wide_check(param),
234 )],
235 RTypeCheck::Vector(r_type) => vec![RAssertion::new(
236 format!("'{}' must be {}", param, r_type),
237 format!("is.{}({})", r_type, param),
238 )],
239 RTypeCheck::Nullable(inner) => inner
240 .assertions(param)
241 .into_iter()
242 .map(|a| a.nullable(param))
243 .collect(),
244 RTypeCheck::List => vec![RAssertion::new(
245 format!("'{}' must be a list", param),
246 format!("is.list({})", param),
247 )],
248 }
249 }
250
251 fn coerced(self) -> Self {
261 match self {
262 RTypeCheck::VectorIntegerWide => RTypeCheck::VectorIntegerStrict,
263 RTypeCheck::Nullable(inner) => RTypeCheck::Nullable(Box::new(inner.coerced())),
264 other => other,
265 }
266 }
267}
268
269fn coerce_tightened(check: RTypeCheck, ty: &syn::Type) -> RTypeCheck {
280 let syn::Type::Path(tp) = ty else {
281 return check.coerced();
282 };
283 let Some(seg) = tp.path.segments.last() else {
284 return check.coerced();
285 };
286 match seg.ident.to_string().as_str() {
287 "bool" => match check {
288 RTypeCheck::Scalar(_) => RTypeCheck::Scalar("integer"),
289 other => other.coerced(),
290 },
291 "Vec" => match extract_single_generic_arg(seg) {
293 Some(syn::Type::Path(inner))
294 if inner
295 .path
296 .segments
297 .last()
298 .is_some_and(|s| s.ident == "bool") =>
299 {
300 RTypeCheck::VectorIntegerStrict
301 }
302 _ => check.coerced(),
303 },
304 "Option" => match (check, extract_single_generic_arg(seg)) {
305 (RTypeCheck::Nullable(inner_check), Some(inner_ty)) => {
306 RTypeCheck::Nullable(Box::new(coerce_tightened(*inner_check, inner_ty)))
307 }
308 (other, _) => other.coerced(),
309 },
310 _ => check.coerced(),
311 }
312}
313
314fn r_check_for_type(ty: &syn::Type) -> Option<RTypeCheck> {
318 match ty {
319 syn::Type::Path(type_path) => r_check_for_type_path(type_path),
320 syn::Type::Reference(type_ref) => r_check_for_reference(type_ref),
321 _ => None,
322 }
323}
324
325fn r_check_for_type_path(type_path: &syn::TypePath) -> Option<RTypeCheck> {
331 let segment = type_path.path.segments.last()?;
332 let ident = segment.ident.to_string();
333
334 match ident.as_str() {
335 "i32" | "f64" | "f32" | "i8" | "i16" | "i64" | "isize" => Some(RTypeCheck::ScalarNumeric),
337
338 "u16" | "u32" | "u64" | "usize" => Some(RTypeCheck::ScalarNonNeg),
340
341 "bool" | "Rbool" | "Rboolean" => Some(RTypeCheck::Scalar("logical")),
343
344 "String" | "char" | "PathBuf" => Some(RTypeCheck::Scalar("character")),
346
347 "u8" => Some(RTypeCheck::Scalar("raw")),
349
350 "Rcomplex" => Some(RTypeCheck::Scalar("complex")),
352
353 "Option" => {
355 let inner_ty = extract_single_generic_arg(segment)?;
356 r_check_for_type(inner_ty).map(|inner| RTypeCheck::Nullable(Box::new(inner)))
357 }
358
359 "Vec" => {
361 let inner_ty = extract_single_generic_arg(segment)?;
362 r_check_for_vec_element(inner_ty)
363 }
364
365 "HashMap" | "BTreeMap" | "NamedList" => Some(RTypeCheck::List),
367
368 "List" | "ListMut" => Some(RTypeCheck::List),
370
371 "SEXP" | "Dots" | "Missing" | "ExternalPtr" | "OwnedProtect" => None,
373
374 _ => None,
376 }
377}
378
379fn r_check_for_reference(type_ref: &syn::TypeReference) -> Option<RTypeCheck> {
384 match type_ref.elem.as_ref() {
385 syn::Type::Path(tp) => {
387 let seg = tp.path.segments.last()?;
388 match seg.ident.to_string().as_str() {
389 "str" => Some(RTypeCheck::Scalar("character")),
390 "Path" => Some(RTypeCheck::Scalar("character")),
391 "Dots" => None,
392 _ => None,
393 }
394 }
395 syn::Type::Slice(slice) => r_check_for_vec_element(&slice.elem),
397 _ => None,
398 }
399}
400
401fn r_check_for_vec_element(elem_ty: &syn::Type) -> Option<RTypeCheck> {
407 let syn::Type::Path(tp) = elem_ty else {
408 return None;
409 };
410 let seg = tp.path.segments.last()?;
411 let ident = seg.ident.to_string();
412
413 match ident.as_str() {
414 "f64" | "f32" => Some(RTypeCheck::VectorNumeric),
416
417 "i32" => Some(RTypeCheck::VectorIntegerStrict),
425
426 "i8" | "i16" | "u16" | "u32" | "i64" | "u64" | "isize" | "usize" => {
435 Some(RTypeCheck::VectorIntegerWide)
436 }
437
438 "bool" => Some(RTypeCheck::Vector("logical")),
440
441 "String" => Some(RTypeCheck::Vector("character")),
443
444 "u8" => Some(RTypeCheck::Vector("raw")),
446
447 "Rcomplex" => Some(RTypeCheck::Vector("complex")),
449
450 "Option" => {
452 let inner = extract_single_generic_arg(seg)?;
453 r_check_for_vec_element(inner)
455 }
456
457 _ => None,
458 }
459}
460
461fn extract_single_generic_arg(segment: &syn::PathSegment) -> Option<&syn::Type> {
465 if let syn::PathArguments::AngleBracketed(ref args) = segment.arguments
466 && let Some(syn::GenericArgument::Type(ty)) = args.args.first()
467 {
468 return Some(ty);
469 }
470 None
471}
472
473#[allow(dead_code)] pub struct FallbackParam {
479 pub r_name: String,
481}
482
483pub struct PreconditionOutput {
488 pub static_checks: Vec<String>,
494 #[allow(dead_code)] pub fallback_params: Vec<FallbackParam>,
497}
498
499fn is_skip_type(ident: &str) -> bool {
505 matches!(
506 ident,
507 "SEXP" | "Dots" | "Missing" | "ExternalPtr" | "OwnedProtect"
508 )
509}
510
511fn needs_fallback(ty: &syn::Type) -> bool {
517 match ty {
518 syn::Type::Path(tp) => {
519 let Some(seg) = tp.path.segments.last() else {
520 return false;
521 };
522 !is_skip_type(&seg.ident.to_string())
523 }
524 syn::Type::Reference(_) => false,
526 _ => false,
527 }
528}
529
530pub fn build_precondition_checks(
549 inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
550 skip_params: &HashSet<String>,
551 opts: &PreconditionOptions,
552) -> PreconditionOutput {
553 let mut args = Vec::new();
554 let mut fallback_params = Vec::new();
555
556 for arg in inputs {
557 let syn::FnArg::Typed(pt) = arg else {
559 continue;
560 };
561
562 let syn::Pat::Ident(pat_ident) = pt.pat.as_ref() else {
564 continue;
565 };
566
567 let r_name = crate::r_wrapper_builder::normalize_r_arg_ident(&pat_ident.ident).to_string();
569
570 if skip_params.contains(&r_name) {
572 continue;
573 }
574
575 if let Some(mut check) = r_check_for_type(pt.ty.as_ref()) {
579 if opts.is_coerced(&r_name) {
580 check = coerce_tightened(check, pt.ty.as_ref());
581 }
582 for assertion in check.assertions(&r_name) {
583 args.push(assertion.to_stopifnot_arg());
584 }
585 } else if needs_fallback(pt.ty.as_ref()) {
586 fallback_params.push(FallbackParam { r_name });
588 }
589 }
590
591 let static_checks = match args.len() {
592 0 => Vec::new(),
593 1 => vec![format!("stopifnot({})", args[0])],
594 _ => {
595 let mut lines = Vec::with_capacity(args.len() + 2);
596 lines.push("stopifnot(".to_string());
597 for (i, arg) in args.iter().enumerate() {
598 let comma = if i < args.len() - 1 { "," } else { "" };
599 lines.push(format!(" {}{}", arg, comma));
600 }
601 lines.push(")".to_string());
602 lines
603 }
604 };
605
606 PreconditionOutput {
607 static_checks,
608 fallback_params,
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615
616 fn parse_type(s: &str) -> syn::Type {
618 syn::parse_str(s).unwrap()
619 }
620
621 fn assertions_for(ty_str: &str, param: &str) -> Vec<RAssertion> {
623 let ty = parse_type(ty_str);
624 r_check_for_type(&ty).unwrap().assertions(param)
625 }
626
627 #[test]
628 fn scalar_numeric_produces_two_assertions() {
629 let asserts = assertions_for("i32", "x");
630 assert_eq!(asserts.len(), 2);
631 assert_eq!(asserts[0].message, "'x' must be numeric, logical, or raw");
632 assert_eq!(
633 asserts[0].condition,
634 "is.numeric(x) || is.logical(x) || is.raw(x)"
635 );
636 assert_eq!(asserts[1].message, "'x' must have length 1");
637 assert_eq!(asserts[1].condition, "length(x) == 1L");
638 }
639
640 #[test]
641 fn all_signed_numeric_types_use_scalar_numeric() {
642 for ty_str in &["i32", "f64", "f32", "i8", "i16", "i64", "isize"] {
643 let asserts = assertions_for(ty_str, "x");
644 assert_eq!(asserts.len(), 2, "{} should produce 2 assertions", ty_str);
645 assert!(
646 asserts[0].condition.contains("is.numeric(x)"),
647 "{} type check",
648 ty_str
649 );
650 assert!(
651 asserts[0].condition.contains("is.logical(x)"),
652 "{} accepts logical",
653 ty_str
654 );
655 assert!(
656 asserts[0].condition.contains("is.raw(x)"),
657 "{} accepts raw",
658 ty_str
659 );
660 }
661 }
662
663 #[test]
664 fn scalar_non_neg_produces_three_assertions() {
665 let asserts = assertions_for("u32", "n");
666 assert_eq!(asserts.len(), 3);
667 assert_eq!(asserts[0].message, "'n' must be numeric, logical, or raw");
668 assert_eq!(asserts[1].message, "'n' must have length 1");
669 assert_eq!(asserts[2].message, "'n' must be non-negative");
670 assert_eq!(asserts[2].condition, "is.raw(n) || n >= 0");
671 }
672
673 #[test]
674 fn all_unsigned_types_use_scalar_non_neg() {
675 for ty_str in &["u16", "u32", "u64", "usize"] {
676 let asserts = assertions_for(ty_str, "x");
677 assert_eq!(asserts.len(), 3, "{} should produce 3 assertions", ty_str);
678 assert!(
679 asserts[2].condition.contains(">= 0"),
680 "{} non-neg check",
681 ty_str
682 );
683 }
684 }
685
686 #[test]
687 fn scalar_logical() {
688 let asserts = assertions_for("bool", "x");
689 assert_eq!(asserts.len(), 2);
690 assert_eq!(asserts[0].message, "'x' must be logical");
691 assert_eq!(asserts[0].condition, "is.logical(x)");
692 assert_eq!(asserts[1].condition, "length(x) == 1L");
693 }
694
695 #[test]
696 fn scalar_character() {
697 for ty_str in &["String", "char", "PathBuf"] {
698 let asserts = assertions_for(ty_str, "s");
699 assert_eq!(asserts.len(), 2);
700 assert_eq!(asserts[0].message, "'s' must be character");
701 assert_eq!(asserts[0].condition, "is.character(s)");
702 }
703 }
704
705 #[test]
706 fn ref_str() {
707 let ty: syn::Type = syn::parse_str("& str").unwrap();
708 let asserts = r_check_for_type(&ty).unwrap().assertions("s");
709 assert_eq!(asserts.len(), 2);
710 assert_eq!(asserts[0].condition, "is.character(s)");
711 }
712
713 #[test]
714 fn scalar_raw() {
715 let asserts = assertions_for("u8", "x");
716 assert_eq!(asserts.len(), 2);
717 assert_eq!(asserts[0].message, "'x' must be raw");
718 assert_eq!(asserts[0].condition, "is.raw(x)");
719 }
720
721 #[test]
722 fn vector_float_stays_loose() {
723 for ty_str in &["Vec<f64>", "Vec<f32>"] {
725 let asserts = assertions_for(ty_str, "x");
726 assert_eq!(asserts.len(), 1, "{} should produce 1 assertion", ty_str);
727 assert_eq!(
728 asserts[0].condition,
729 "is.numeric(x) || is.logical(x) || is.raw(x)"
730 );
731 }
732 }
733
734 #[test]
735 fn vector_i32_is_intsxp_strict() {
736 let asserts = assertions_for("Vec<i32>", "x");
740 assert_eq!(asserts.len(), 1);
741 assert_eq!(asserts[0].condition, "is.integer(x)");
742 assert_eq!(asserts[0].message, "'x' must be an integer vector");
743 }
744
745 #[test]
746 fn vector_integer_wide_is_lossless() {
747 for ty_str in &[
750 "Vec<i8>",
751 "Vec<i16>",
752 "Vec<u16>",
753 "Vec<u32>",
754 "Vec<i64>",
755 "Vec<u64>",
756 "Vec<isize>",
757 "Vec<usize>",
758 ] {
759 let asserts = assertions_for(ty_str, "x");
760 assert_eq!(asserts.len(), 1, "{} should produce 1 assertion", ty_str);
761 assert_eq!(
762 asserts[0].condition,
763 "is.integer(x) || is.logical(x) || is.raw(x) || \
764 (is.numeric(x) && all(is.na(x) | x == trunc(x)))",
765 "{}",
766 ty_str
767 );
768 assert_eq!(
769 asserts[0].message,
770 "'x' must be integer or whole-number numeric"
771 );
772 }
773 }
774
775 #[test]
776 fn slice_int_is_strict() {
777 let ty: syn::Type = syn::parse_str("& [i32]").unwrap();
778 let asserts = r_check_for_type(&ty).unwrap().assertions("x");
779 assert_eq!(asserts.len(), 1);
780 assert_eq!(asserts[0].condition, "is.integer(x)");
781 }
782
783 #[test]
784 fn coerced_wide_integer_vec_tightens_to_strict() {
785 let check = r_check_for_type(&parse_type("Vec<u16>")).unwrap();
788 let coerced = check.coerced();
789 let asserts = coerced.assertions("x");
790 assert_eq!(asserts.len(), 1);
791 assert_eq!(asserts[0].condition, "is.integer(x)");
792 }
793
794 #[test]
795 fn coerced_does_not_change_float_or_i32() {
796 let i32_coerced = r_check_for_type(&parse_type("Vec<i32>")).unwrap().coerced();
798 assert_eq!(i32_coerced.assertions("x")[0].condition, "is.integer(x)");
799 let f64_coerced = r_check_for_type(&parse_type("Vec<f64>")).unwrap().coerced();
800 assert_eq!(
801 f64_coerced.assertions("x")[0].condition,
802 "is.numeric(x) || is.logical(x) || is.raw(x)"
803 );
804 }
805
806 #[test]
807 fn build_checks_coerced_param_uses_is_integer() {
808 let sig: syn::Signature = syn::parse_str("fn f(x: Vec<u16>)").unwrap();
809 let mut coerce_params = HashSet::new();
810 coerce_params.insert("x".to_string());
811 let opts = PreconditionOptions {
812 coerce_all: false,
813 coerce_params,
814 };
815 let output = build_precondition_checks(&sig.inputs, &HashSet::new(), &opts);
816 let joined = output.static_checks.join("\n");
817 assert!(joined.contains("is.integer(x)"));
818 assert!(!joined.contains("trunc(x)"));
819 }
820
821 #[test]
822 fn vector_character() {
823 let asserts = assertions_for("Vec<String>", "x");
824 assert_eq!(asserts.len(), 1);
825 assert_eq!(asserts[0].condition, "is.character(x)");
826 }
827
828 #[test]
829 fn vector_optional_string() {
830 let asserts = assertions_for("Vec<Option<String>>", "x");
831 assert_eq!(asserts.len(), 1);
832 assert_eq!(asserts[0].condition, "is.character(x)");
833 }
834
835 #[test]
836 fn slice_u8() {
837 let ty: syn::Type = syn::parse_str("& [u8]").unwrap();
838 let asserts = r_check_for_type(&ty).unwrap().assertions("x");
839 assert_eq!(asserts.len(), 1);
840 assert_eq!(asserts[0].condition, "is.raw(x)");
841 }
842
843 #[test]
844 fn nullable_wraps_inner_assertions() {
845 let asserts = assertions_for("Option<i32>", "x");
846 assert_eq!(asserts.len(), 2);
847 assert_eq!(
848 asserts[0].message,
849 "'x' must be NULL or numeric, logical, or raw"
850 );
851 assert_eq!(
852 asserts[0].condition,
853 "is.null(x) || is.numeric(x) || is.logical(x) || is.raw(x)"
854 );
855 assert_eq!(asserts[1].message, "'x' must be NULL or have length 1");
856 assert_eq!(asserts[1].condition, "is.null(x) || length(x) == 1L");
857 }
858
859 #[test]
860 fn nullable_character() {
861 let asserts = assertions_for("Option<String>", "s");
862 assert_eq!(asserts.len(), 2);
863 assert_eq!(asserts[0].message, "'s' must be NULL or character");
864 assert_eq!(asserts[0].condition, "is.null(s) || is.character(s)");
865 assert_eq!(asserts[1].message, "'s' must be NULL or have length 1");
866 }
867
868 #[test]
869 fn map_types() {
870 for ty_str in &["HashMap<String, i32>", "BTreeMap<String, f64>"] {
871 let ty = parse_type(ty_str);
872 let asserts = r_check_for_type(&ty).unwrap().assertions("x");
873 assert_eq!(asserts.len(), 1);
874 assert_eq!(asserts[0].condition, "is.list(x)");
875 }
876 }
877
878 #[test]
879 fn skip_types() {
880 for ty_str in &["SEXP", "ExternalPtr<MyType>"] {
881 let ty = parse_type(ty_str);
882 assert!(
883 r_check_for_type(&ty).is_none(),
884 "{} should be skipped",
885 ty_str
886 );
887 }
888 }
889
890 #[test]
891 fn single_param_produces_multi_line() {
892 let sig: syn::Signature = syn::parse_str("fn f(n: i32)").unwrap();
894 let output = build_precondition_checks(
895 &sig.inputs,
896 &HashSet::new(),
897 &PreconditionOptions::default(),
898 );
899 let checks = &output.static_checks;
900 assert_eq!(checks.len(), 4); assert_eq!(checks[0], "stopifnot(");
902 assert!(checks[1].contains("numeric, logical, or raw"));
903 assert!(checks[2].contains("length 1"));
904 assert_eq!(checks[3], ")");
905 assert!(output.fallback_params.is_empty());
906 }
907
908 #[test]
909 fn vector_param_single_line() {
910 let sig: syn::Signature = syn::parse_str("fn f(x: Vec<f64>)").unwrap();
912 let output = build_precondition_checks(
913 &sig.inputs,
914 &HashSet::new(),
915 &PreconditionOptions::default(),
916 );
917 let checks = &output.static_checks;
918 assert_eq!(checks.len(), 1);
919 assert!(checks[0].starts_with("stopifnot("));
920 assert!(checks[0].ends_with(')'));
921 }
922
923 #[test]
924 fn two_scalar_params_produces_six_lines() {
925 let sig: syn::Signature = syn::parse_str("fn f(a: i32, b: f64)").unwrap();
926 let output = build_precondition_checks(
927 &sig.inputs,
928 &HashSet::new(),
929 &PreconditionOptions::default(),
930 );
931 let checks = &output.static_checks;
932 assert_eq!(checks.len(), 6);
934 assert_eq!(checks[0], "stopifnot(");
935 assert!(checks[1].contains("'a'") && checks[1].contains("numeric"));
936 assert!(checks[2].contains("'a'") && checks[2].contains("length 1"));
937 assert!(checks[3].contains("'b'") && checks[3].contains("numeric"));
938 assert!(checks[4].contains("'b'") && checks[4].contains("length 1"));
939 assert_eq!(checks[5], ")");
940 }
941
942 #[test]
943 fn build_checks_skips_match_arg() {
944 let sig: syn::Signature = syn::parse_str("fn f(n: i32, mode: String)").unwrap();
945 let mut skip = HashSet::new();
946 skip.insert("mode".to_string());
947 let output = build_precondition_checks(&sig.inputs, &skip, &PreconditionOptions::default());
948 let joined = output.static_checks.join("\n");
950 assert!(joined.contains("'n'"));
951 assert!(!joined.contains("'mode'"));
952 }
953
954 #[test]
955 fn unknown_type_produces_fallback() {
956 let sig: syn::Signature = syn::parse_str("fn f(x: MyCustomType)").unwrap();
957 let output = build_precondition_checks(
958 &sig.inputs,
959 &HashSet::new(),
960 &PreconditionOptions::default(),
961 );
962 assert!(output.static_checks.is_empty());
963 assert_eq!(output.fallback_params.len(), 1);
964 assert_eq!(output.fallback_params[0].r_name, "x");
965 }
966
967 #[test]
968 fn mixed_known_and_unknown_types() {
969 let sig: syn::Signature = syn::parse_str("fn f(a: i32, b: MyType, c: String)").unwrap();
970 let output = build_precondition_checks(
971 &sig.inputs,
972 &HashSet::new(),
973 &PreconditionOptions::default(),
974 );
975 let joined = output.static_checks.join("\n");
977 assert!(joined.contains("'a'"));
978 assert!(joined.contains("'c'"));
979 assert!(!joined.contains("'b'"));
980 assert_eq!(output.fallback_params.len(), 1);
982 assert_eq!(output.fallback_params[0].r_name, "b");
983 }
984
985 #[test]
986 fn sexp_not_fallback() {
987 let sig: syn::Signature = syn::parse_str("fn f(x: SEXP)").unwrap();
988 let output = build_precondition_checks(
989 &sig.inputs,
990 &HashSet::new(),
991 &PreconditionOptions::default(),
992 );
993 assert!(output.static_checks.is_empty());
994 assert!(output.fallback_params.is_empty());
995 }
996}