1use num_bigint::{BigInt, Sign};
2use proc_macro2::{TokenStream, TokenTree};
3use prop::collection::VecStrategy;
4use proptest::prelude::*;
5use serde::{Deserialize, Serialize};
6use syn::{FnArg, ItemFn};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub(crate) enum Type {
10 String,
11 Float,
12 Cons,
13 Symbol,
14 Integer,
15 PosInteger,
16 Boolean,
17 True,
18 False,
19 Unknown,
20 Function,
21 UnibyteString,
22 Vector,
23 HashTable,
24 Record,
25 ByteFn,
26 Byte,
27 Subr,
28 Buffer,
29 Nil,
30 Char,
31 CharTable,
32 BigInt,
33 CustomString(String),
34 Multiple(Vec<Type>),
35 CustomList(Vec<Type>),
36}
37
38impl Type {
39 const SYMBOL_CHARS: &'static str = "[a-zA-Z][a-zA-Z0-9-]*";
40 const MAX_FIXNUM: i64 = i64::MAX >> 8;
41 const MIN_FIXNUM: i64 = i64::MIN >> 8;
42 fn strategy(self) -> BoxedStrategy<ArbitraryType> {
44 match self {
45 Type::String => any::<String>().prop_map(ArbitraryType::String).boxed(),
46 Type::Float => any::<f64>().prop_map(ArbitraryType::Float).boxed(),
47 Type::Cons => Self::cons_strategy().prop_map(ArbitraryType::Cons).boxed(),
48 Type::Symbol => Self::SYMBOL_CHARS.prop_map(ArbitraryType::Symbol).boxed(),
49 Type::Integer => Self::fixnum_strategy().prop_map(ArbitraryType::Integer).boxed(),
50 Type::PosInteger => {
51 Self::pos_fixnum_strategy().prop_map(ArbitraryType::Integer).boxed()
52 }
53 Type::Boolean => any::<bool>().prop_map(ArbitraryType::Boolean).boxed(),
54 Type::True => Just(true).prop_map(ArbitraryType::Boolean).boxed(),
55 Type::False => Just(false).prop_map(ArbitraryType::Boolean).boxed(),
56 Type::Unknown => Self::any_object_strategy(),
57 Type::UnibyteString => "[a-zA-Z0-9 ]*".prop_map(ArbitraryType::UnibyteString).boxed(),
58 Type::Vector => prop::collection::vec(Self::any_object_strategy(), 0..10)
59 .prop_map(ArbitraryType::Vector)
60 .boxed(),
61 Type::Record => {
62 (Self::SYMBOL_CHARS, prop::collection::vec(Self::any_object_strategy(), 0..10))
63 .prop_map(ArbitraryType::Record)
64 .boxed()
65 }
66 Type::HashTable => todo!("Strategy for HashTable not implemented"),
67 Type::ByteFn => any::<u8>().prop_map(ArbitraryType::ByteFn).boxed(),
68 Type::Byte => any::<u8>().prop_map(ArbitraryType::Byte).boxed(),
69 Type::Subr => todo!("Strategy for Subr not implemented"),
70 Type::Buffer => any::<String>().prop_map(ArbitraryType::Buffer).boxed(),
71 Type::Nil => Just(ArbitraryType::Nil).boxed(),
72 Type::Char => any::<char>().prop_map(ArbitraryType::Char).boxed(),
73 Type::CharTable => todo!("Strategy for CharTable not implemented"),
74 Type::Function => todo!("Strategy for Function not implemented"),
75 Type::Multiple(s) => combined_strategy(&s),
76 Type::CustomString(s) => proptest::string::string_regex(&s)
77 .expect("Invalid proptest regex")
78 .prop_map(ArbitraryType::String)
79 .boxed(),
80 Type::CustomList(list) => {
81 let arb_list: Vec<_> = list.iter().map(|x| x.clone().strategy()).collect();
82 (arb_list, Just(false)).prop_map(ArbitraryType::Cons).boxed()
83 }
84 Type::BigInt => Self::big_int_strategy().prop_map(ArbitraryType::BigInt).boxed(),
85 }
86 }
87
88 fn fixnum_strategy() -> BoxedStrategy<i64> {
89 any::<i64>()
90 .prop_filter("Fixnum", |x| *x >= Self::MIN_FIXNUM && *x <= Self::MAX_FIXNUM)
91 .boxed()
92 }
93
94 fn pos_fixnum_strategy() -> BoxedStrategy<i64> {
95 any::<i64>()
96 .prop_filter("Fixnum", |x| *x >= 0 && *x <= Self::MAX_FIXNUM)
97 .boxed()
98 }
99
100 fn big_int_strategy() -> BoxedStrategy<BigInt> {
101 (any::<bool>(), any::<Vec<u32>>())
102 .prop_map(|(s, v)| {
103 let sign = if s { Sign::Plus } else { Sign::Minus };
104 BigInt::from_slice(sign, &v)
105 })
106 .boxed()
107 }
108
109 fn cons_strategy() -> (VecStrategy<BoxedStrategy<ArbitraryType>>, BoxedStrategy<bool>) {
110 (
111 prop::collection::vec(Self::any_object_strategy(), 0..10),
112 prop_oneof![
113 1 => Just(true),
114 3 => Just(false),
115 ]
116 .boxed(),
117 )
118 }
119
120 pub(crate) fn any_object_strategy() -> BoxedStrategy<ArbitraryType> {
121 prop_oneof![
122 Just(ArbitraryType::Nil),
123 any::<bool>().prop_map(ArbitraryType::Boolean),
124 Self::fixnum_strategy().prop_map(ArbitraryType::Integer),
125 any::<f64>().prop_map(ArbitraryType::Float),
126 any::<String>().prop_map(ArbitraryType::String),
127 Self::SYMBOL_CHARS.prop_map(ArbitraryType::Symbol),
128 "[a-zA-Z0-9 ]*".prop_map(ArbitraryType::UnibyteString),
129 any::<char>().prop_map(ArbitraryType::Char),
130 ]
131 .boxed()
132 }
133}
134
135pub(crate) fn combined_strategy(types: &[Type]) -> BoxedStrategy<ArbitraryType> {
137 match types.len() {
139 0 => panic!("At least one type must be provided"),
140 1 => types[0].clone().strategy(),
141 2 => prop_oneof![types[0].clone().strategy(), types[1].clone().strategy()].boxed(),
142 3 => prop_oneof![
143 types[0].clone().strategy(),
144 types[1].clone().strategy(),
145 types[2].clone().strategy()
146 ]
147 .boxed(),
148 4 => prop_oneof![
149 types[0].clone().strategy(),
150 types[1].clone().strategy(),
151 types[2].clone().strategy(),
152 types[3].clone().strategy()
153 ]
154 .boxed(),
155 5 => prop_oneof![
156 types[0].clone().strategy(),
157 types[1].clone().strategy(),
158 types[2].clone().strategy(),
159 types[3].clone().strategy(),
160 types[4].clone().strategy()
161 ]
162 .boxed(),
163 n => panic!("Currently supporting up to 5 combined types, got {n}"),
164 }
165}
166
167#[derive(Clone, PartialEq, PartialOrd, Debug, Serialize, Deserialize)]
168pub(crate) enum ArbitraryType {
169 String(String),
170 Float(f64),
171 Cons((Vec<ArbitraryType>, bool)),
172 Symbol(String),
173 Integer(i64),
174 Boolean(bool),
175 Unknown(Box<ArbitraryType>),
176 UnibyteString(String),
177 Vector(Vec<ArbitraryType>),
178 HashTable(Vec<(ArbitraryType, ArbitraryType)>),
179 Record((String, Vec<ArbitraryType>)),
180 Nil,
181 Function(u8),
182 ByteFn(u8),
183 Byte(u8),
184 Char(char),
185 Buffer(String),
186 Subr(u8),
187 BigInt(BigInt),
188}
189
190pub(crate) fn print_args(args: &[Option<ArbitraryType>]) -> String {
191 args.iter()
192 .map(|x| match x {
193 Some(x) => format!("{x}"),
194 None => "nil".to_owned(),
195 })
196 .collect::<Vec<_>>()
197 .join(" ")
198}
199
200impl std::fmt::Display for ArbitraryType {
201 #[expect(clippy::too_many_lines)]
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 use std::string::ToString;
204 match self {
205 ArbitraryType::String(s) => {
206 write!(f, "\"")?;
207 for c in s.chars() {
208 match c {
209 '\n' => write!(f, "\\n")?,
210 '\t' => write!(f, "\\t")?,
211 '\r' => write!(f, "\\r")?,
212 '\\' => write!(f, "\\\\")?,
213 '\"' => write!(f, "\\\"")?,
214 c => write!(f, "{c}")?,
215 }
216 }
217 write!(f, "\"")
218 }
219 ArbitraryType::Float(n) => {
220 if n.fract() == 0.0_f64 {
221 write!(f, "{n:.1}")
222 } else {
223 write!(f, "{n}")
224 }
225 }
226 ArbitraryType::Cons(list) => {
227 let mut cells: Vec<_> = list.0.iter().map(ToString::to_string).collect();
228 let len = list.0.len();
229 let dot_end = list.1;
230 if dot_end && len >= 2 {
231 cells.insert(len - 1, ".".to_owned());
232 }
233 let string = cells.join(" ");
234 write!(f, "'({string})")
235 }
236 ArbitraryType::Symbol(s) => write!(f, "'{s}"),
237 ArbitraryType::Integer(n) => write!(f, "{n}"),
238 ArbitraryType::Boolean(b) => {
239 if *b {
240 write!(f, "t")
241 } else {
242 write!(f, "nil")
243 }
244 }
245 ArbitraryType::Unknown(obj) => write!(f, "{obj}"),
246 ArbitraryType::UnibyteString(s) => {
247 write!(f, "\"")?;
248 for c in s.chars() {
249 match c {
250 '\n' => write!(f, "\\n")?,
251 '\t' => write!(f, "\\t")?,
252 '\r' => write!(f, "\\r")?,
253 '\\' => write!(f, "\\\\")?,
254 '"' => write!(f, "\\\"")?,
255 c => write!(f, "{c}")?,
256 }
257 }
258 write!(f, "\"")
259 }
260 ArbitraryType::Nil => write!(f, "nil"),
261 ArbitraryType::Vector(vec) => {
262 let cells: Vec<_> = vec.iter().map(ToString::to_string).collect();
263 let string = cells.join(" ");
264 write!(f, "[{string}]")
265 }
266 ArbitraryType::HashTable(vec) => {
267 write!(f, "#s(hash-table data (")?;
268 for (key, value) in vec {
269 write!(f, "{key} {value} ")?;
270 }
271 write!(f, "))")
272 }
273 ArbitraryType::Record((name, members)) => {
274 let cells: Vec<_> = members.iter().map(ToString::to_string).collect();
275 let string = cells.join(" ");
276 write!(f, "(record '{name} {string})")
277 }
278 ArbitraryType::Function(arity) => {
279 write!(f, "(lambda (")?;
280 for i in 0..*arity {
281 write!(f, "arg{i} ")?;
282 }
283 write!(f, ") nil)")
284 }
285 ArbitraryType::ByteFn(arity) => {
286 write!(f, "(lambda (")?;
287 for i in 0..*arity {
288 write!(f, "arg{i} ")?;
289 }
290 write!(f, ") nil)")
291 }
292 ArbitraryType::Byte(n) => write!(f, "{n}"),
293 ArbitraryType::Buffer(name) => {
294 write!(f, "(generate-new-buffer {name})")
295 }
296 ArbitraryType::Subr(arity) => {
297 write!(f, "(lambda (")?;
298 for i in 0..*arity {
299 write!(f, "arg{i} ")?;
300 }
301 write!(f, ") nil)")
302 }
303 ArbitraryType::Char(chr) => match chr {
304 '\n' => write!(f, "?\\n"),
305 '\t' => write!(f, "?\\t"),
306 '\r' => write!(f, "?\\r"),
307 '\u{0B}' => write!(f, "?\\v"),
308 '\u{0C}' => write!(f, "?\\f"),
309 '\u{1B}' => write!(f, "?\\e"),
310 '\u{7F}' => write!(f, "?\\d"),
311 '\u{08}' => write!(f, "?\\b"),
312 '\u{07}' => write!(f, "?\\a"),
313 '(' | ')' | '[' | ']' | '\\' | '"' => write!(f, "?\\{chr}"),
314 chr => write!(f, "?{chr}"),
315 },
316 ArbitraryType::BigInt(n) => write!(f, "{n}"),
317 }
318 }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub(crate) enum ArgType {
323 Required(Type),
324 Optional(Type),
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub(crate) struct Function {
329 pub(crate) name: String,
330 pub(crate) args: Vec<ArgType>,
331 pub(crate) ret: Option<Type>,
332 pub(crate) fallible: bool,
333}
334
335#[derive(Debug, Serialize, Deserialize)]
336pub(crate) struct Config {
337 pub(crate) test_count: u32,
338 pub(crate) functions: Vec<Function>,
339}
340
341#[allow(dead_code)]
342impl Function {
343 pub(crate) fn strategy(self) -> Vec<BoxedStrategy<Option<ArbitraryType>>> {
344 self.args
345 .into_iter()
346 .map(|arg| match arg {
347 ArgType::Required(ty) => ty.strategy().prop_map(Some).boxed(),
348 ArgType::Optional(ty) => {
349 prop_oneof![1 => Just(None), 3 => ty.strategy().prop_map(Some)].boxed()
350 }
351 })
352 .collect::<Vec<_>>()
353 }
354
355 fn process_arg(ty: &syn::Type) -> Result<Type, String> {
356 match ty {
357 syn::Type::Array(_) => Err("Array not supported".to_string()),
358 syn::Type::BareFn(_) => Err("BareFn not supported".to_string()),
359 syn::Type::ImplTrait(_) => Err("Impl Trait not supported".to_string()),
360 syn::Type::Infer(_) => Err("Infer not supported".to_string()),
361 syn::Type::Macro(_) => Err("Macro not supported".to_string()),
362 syn::Type::Never(_) => Err("Never not supported".to_string()),
363 syn::Type::Paren(_) => Err("Paren not supported".to_string()),
364 syn::Type::Path(syn::TypePath { path, .. }) => {
365 let segments = &path.segments;
366 let last = segments.last().unwrap().ident.to_string();
367
368 match Self::get_simple_type(&last) {
369 Some(x) => Ok(x),
370 None => match last.as_str() {
371 "Rto" | "Rt" | "Gc" => Self::process_arg(get_generic_arg(segments)?),
372 s @ ("Env" | "Context") => Err(format!("{s} type not supported")),
373 s => Err(format!("Unknown type: {s}")),
374 },
375 }
376 }
377 syn::Type::Ptr(_) => Err("Ptr not supported".to_string()),
378 syn::Type::Reference(syn::TypeReference { elem, .. })
379 | syn::Type::Group(syn::TypeGroup { elem, .. }) => Function::process_arg(elem),
380 syn::Type::Slice(syn::TypeSlice { .. }) => Ok(Type::Cons),
381 syn::Type::TraitObject(_) => Err("TraitObject not supported".to_string()),
382 syn::Type::Tuple(_) => Ok(Type::Nil),
383 syn::Type::Verbatim(_) => Err("Verbatim type not supported".to_string()),
384 _ => Err("Unknown type".to_string()),
385 }
386 }
387
388 fn get_simple_type(ty: &str) -> Option<Type> {
389 Some(match ty {
390 "StringOrChar" => Type::Multiple(vec![Type::String, Type::Char]),
391 "StringOrSymbol" => Type::Multiple(vec![Type::String, Type::Symbol]),
392 "Symbol" => Type::Symbol,
393 "Number" | "NumberValue" => Type::Multiple(vec![Type::Integer, Type::Float]),
394 "Object" => Type::Unknown,
395 "usize" | "u64" => Type::PosInteger,
396 "u8" => Type::Byte,
397 "isize" | "i64" => Type::Integer,
398 "str" | "String" | "LispString" => Type::String,
399 "bool" | "OptionalFlag" => Type::Boolean,
400 "f64" => Type::Float,
401 "char" => Type::Char,
402 "Function" => Type::Function,
403 "Cons" | "List" | "Error" | "TypeError" | "ArgSlice" => Type::Cons,
404 "LispVec" | "LispVector" | "Vec" | "RecordBuilder" => Type::Vector,
405 "LispHashTable" => Type::HashTable,
406 "CharTable" | "CharTableInner" => Type::CharTable,
407 "ByteString" => Type::UnibyteString,
408 "Record" => Type::Record,
409 "ByteFn" => Type::ByteFn,
410 "SubrFn" => Type::Subr,
411 "LispBuffer" => Type::Buffer,
412 _ => return None,
413 })
414 }
415
416 fn custom_templates(func: &ItemFn) -> Vec<Option<Type>> {
417 for attr in &func.attrs {
418 if let syn::Meta::List(list) = &attr.meta {
419 if list.path.get_ident().unwrap() == "elprop" {
420 let custom_args = Self::parse_template_stream(list.tokens.clone());
421 return custom_args
422 .into_iter()
423 .map(|x| match x {
424 Type::Nil => None,
425 x => Some(x),
426 })
427 .collect();
428 }
429 }
430 }
431 Vec::new()
432 }
433
434 fn parse_template_stream(ts: TokenStream) -> Vec<Type> {
435 let objects = ts.into_iter().fold(vec![Vec::new()], |mut acc, token| {
436 match token {
438 TokenTree::Punct(punct) => match punct.as_char() {
439 ',' => acc.push(Vec::new()),
440 '|' => {}
441 c => panic!("Unexpected punctuation {c}"),
442 },
443 x => acc.last_mut().unwrap().push(Self::parse_template_type(x)),
444 }
445 acc
446 });
447 objects
449 .into_iter()
450 .map(|mut x| if x.len() == 1 { x.pop().unwrap() } else { Type::Multiple(x) })
451 .collect()
452 }
453
454 fn parse_template_type(token: TokenTree) -> Type {
455 match token {
456 TokenTree::Group(group) => {
457 Type::CustomList(Self::parse_template_stream(group.stream()))
458 }
459 TokenTree::Ident(ident) => {
460 let s = ident.to_string();
461 match Self::get_simple_type(&s) {
462 Some(x) => x,
463 None => match &*s {
464 "_" => Type::Nil,
465 "true" => Type::True,
466 "false" => Type::False,
467 s => panic!("Unknown type {s}"),
468 },
469 }
470 }
471 TokenTree::Literal(literal) => {
472 let string = syn::parse_str::<syn::LitStr>(&literal.to_string())
473 .expect("Invalid Literal")
474 .value();
475 if let Err(e) = proptest::string::string_regex(&string) {
476 panic!("{e}: {string}")
477 }
478 Type::CustomString(string)
479 }
480 TokenTree::Punct(p) => {
481 unreachable!("Unexpected punctuation {p}")
482 }
483 }
484 }
485
486 pub(crate) fn from_item(item: &ItemFn) -> Result<Self, String> {
487 let name = item
488 .sig
489 .ident
490 .to_string()
491 .chars()
492 .map(|c| match c {
493 '_' => '-',
494 c => c,
495 })
496 .collect();
497
498 let args = Function::get_args(item);
499
500 let (ret, fallible) = Self::get_output(item)?;
501 Ok(Function { name, args, ret, fallible })
502 }
503
504 fn get_args(item: &ItemFn) -> Vec<ArgType> {
505 let templates = Self::custom_templates(item);
506
507 item.sig
508 .inputs
509 .iter()
510 .map(|x| match x {
511 FnArg::Receiver(syn::Receiver { ty, .. })
512 | FnArg::Typed(syn::PatType { ty, .. }) => ty,
513 })
514 .enumerate()
515 .filter_map(|(i, arg)| {
516 if let Some(Some(template)) = templates.get(i) {
518 return Some(ArgType::Required(template.clone()));
519 }
520 match arg.as_ref() {
521 syn::Type::Group(syn::TypeGroup { group_token, elem }) => {
522 let syn::token::Group { span } = group_token;
523
524 let source_text = span
525 .source_text()
526 .ok_or_else(|| "Failed to get source text".to_string())
527 .ok()?;
528 let optional = matches!(source_text.as_str(), "Option");
529 Self::wrap_arg(optional, elem)
530 }
531 syn::Type::Path(syn::TypePath { path, .. }) => {
532 let segments = &path.segments;
533 let last = segments.last().unwrap().ident.to_string();
534 if matches!(last.as_str(), "Result" | "Option") {
535 let generic_arg = get_generic_arg(segments).ok()?;
536 Self::wrap_arg(true, generic_arg)
537 } else {
538 Self::wrap_arg(false, arg)
539 }
540 }
541 x => Some(ArgType::Required(Function::process_arg(x).ok()?)),
542 }
543 })
544 .collect()
545 }
546
547 fn wrap_arg(optional: bool, ty: &syn::Type) -> Option<ArgType> {
548 let arg = Function::process_arg(ty).ok()?;
549 match arg {
550 Type::Boolean => Some(ArgType::Required(arg)),
551 x => Some(if optional { ArgType::Optional(x) } else { ArgType::Required(x) }),
552 }
553 }
554
555 fn get_output(item: &ItemFn) -> Result<(Option<Type>, bool), String> {
556 Ok(match &item.sig.output {
557 syn::ReturnType::Default => (None, false),
558 syn::ReturnType::Type(_, ty) => match ty.as_ref() {
559 syn::Type::Group(syn::TypeGroup { group_token, elem }) => {
560 let syn::token::Group { span } = group_token;
561
562 let source_text = span
563 .source_text()
564 .ok_or_else(|| "Failed to get source text".to_string())?;
565 let fallible = matches!(source_text.as_str(), "Option" | "Result");
566
567 let ty = Function::process_arg(elem)?;
568 (Some(ty), fallible)
569 }
570 syn::Type::Path(syn::TypePath { path, .. }) => {
571 let segments = &path.segments;
572 let last = segments.last().unwrap().ident.to_string();
573 match last.as_str() {
574 "Result" | "Option" => {
575 let ty = get_generic_arg(segments)?;
576 let ty = Function::process_arg(ty)?;
577 (Some(ty), true)
578 }
579 _ => {
580 let ty = Function::process_arg(ty)?;
581 (Some(ty), false)
582 }
583 }
584 }
585 x => {
586 let ty = Function::process_arg(x)?;
587 (Some(ty), false)
588 }
589 },
590 })
591 }
592}
593
594fn get_generic_arg<'a>(
595 segments: impl IntoIterator<Item = &'a syn::PathSegment>,
596) -> Result<&'a syn::Type, String> {
597 let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
598 ref args, ..
599 }) = segments.into_iter().last().unwrap().arguments
600 else {
601 unreachable!("Expected angle bracketed arguments");
602 };
603
604 args.iter()
605 .fold(None, |acc, arg| match arg {
606 syn::GenericArgument::Type(ty) => Some(ty),
607 _ => acc,
608 })
609 .ok_or_else(|| "Expected type argument".to_string())
610}