1use crucible_macro_utils::{MaxLenConstraint, RangeConstraint};
2use proc_macro::TokenStream;
3use quote::{format_ident, quote};
4use std::collections::HashMap;
5use std::sync::atomic::{AtomicBool, Ordering};
6use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemFn, ItemImpl, PatType, Type};
7
8static FALLBACK_MAIN_EMITTED: AtomicBool = AtomicBool::new(false);
9
10fn read_cargo_features() -> Vec<String> {
11 let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
12 Ok(dir) => dir,
13 Err(_) => return Vec::new(),
14 };
15 let cargo_toml_path = std::path::PathBuf::from(&manifest_dir).join("Cargo.toml");
16 let content = match std::fs::read_to_string(&cargo_toml_path) {
17 Ok(c) => c,
18 Err(_) => return Vec::new(),
19 };
20 let mut features = Vec::new();
21 let mut in_features = false;
22 for line in content.lines() {
23 let trimmed = line.trim();
24 if trimmed == "[features]" {
25 in_features = true;
26 continue;
27 }
28 if in_features && trimmed.starts_with('[') {
29 break;
30 }
31 if in_features && !trimmed.is_empty() && !trimmed.starts_with('#') {
32 if let Some(eq_pos) = trimmed.find('=') {
33 let name = trimmed[..eq_pos].trim();
34 if !name.is_empty() && name != "default" {
35 features.push(name.to_string());
36 }
37 }
38 }
39 }
40 features
41}
42
43enum FieldTypeKind {
45 U8,
46 U16,
47 U32,
48 U64,
49 U128,
50 I8,
51 I16,
52 I32,
53 I64,
54 I128,
55 Usize,
56 Bool,
57 Option(Box<FieldTypeKind>),
58 Vec(Box<FieldTypeKind>),
59}
60
61fn extract_generic_inner<'a>(ty: &'a Type, name: &str) -> Option<&'a Type> {
62 if let Type::Path(tp) = ty {
63 if let Some(seg) = tp.path.segments.last() {
64 if seg.ident == name {
65 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
66 if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
67 return Some(inner);
68 }
69 }
70 }
71 }
72 }
73 None
74}
75
76fn extract_option_inner(ty: &Type) -> Option<&Type> {
77 extract_generic_inner(ty, "Option")
78}
79
80fn extract_vec_inner(ty: &Type) -> Option<&Type> {
81 extract_generic_inner(ty, "Vec")
82}
83
84fn classify_field_type(ty: &Type) -> FieldTypeKind {
85 if let Some(inner) = extract_option_inner(ty) {
86 return FieldTypeKind::Option(Box::new(classify_field_type(inner)));
87 }
88 if let Some(inner) = extract_vec_inner(ty) {
89 return FieldTypeKind::Vec(Box::new(classify_field_type(inner)));
90 }
91 if let Type::Path(tp) = ty {
92 if let Some(seg) = tp.path.segments.last() {
93 return match seg.ident.to_string().as_str() {
94 "u8" => FieldTypeKind::U8,
95 "u16" => FieldTypeKind::U16,
96 "u32" => FieldTypeKind::U32,
97 "u64" => FieldTypeKind::U64,
98 "u128" => FieldTypeKind::U128,
99 "i8" => FieldTypeKind::I8,
100 "i16" => FieldTypeKind::I16,
101 "i32" => FieldTypeKind::I32,
102 "i64" => FieldTypeKind::I64,
103 "i128" => FieldTypeKind::I128,
104 "usize" => FieldTypeKind::Usize,
105 "bool" => FieldTypeKind::Bool,
106 _ => FieldTypeKind::U64,
107 };
108 }
109 }
110 FieldTypeKind::U64
111}
112
113fn field_byte_size(kind: &FieldTypeKind, max_len: Option<usize>) -> usize {
114 match kind {
115 FieldTypeKind::Vec(inner) => {
116 let ml = max_len.unwrap_or(8);
117 let elem_size = match inner.as_ref() {
118 FieldTypeKind::U128 | FieldTypeKind::I128 => 16,
119 _ => 8, };
121 8 + ml * elem_size }
123 FieldTypeKind::U128 | FieldTypeKind::I128 => 16,
124 FieldTypeKind::Option(inner) => match inner.as_ref() {
125 FieldTypeKind::Vec(vec_inner) => {
126 let ml = max_len.unwrap_or(8);
127 let elem_size = match vec_inner.as_ref() {
128 FieldTypeKind::U128 | FieldTypeKind::I128 => 16,
129 _ => 8,
130 };
131 8 + ml * elem_size }
133 FieldTypeKind::U128 | FieldTypeKind::I128 => 16,
134 _ => 8, },
136 _ => 8, }
138}
139
140fn gen_inner_random_expr(
144 inner_kind: &FieldTypeKind,
145 inner_ty: Option<&Type>,
146 constraint: Option<&RangeConstraint>,
147) -> proc_macro2::TokenStream {
148 match (inner_kind, constraint) {
149 (FieldTypeKind::U64, Some(c)) => {
151 let (lo, hi) = c.exclusive_bounds("e! { u64 });
152 quote! { crucible_fuzzer::gen_u64(rng, #lo, #hi) }
153 }
154 (FieldTypeKind::U64, None) => quote! { crucible_fuzzer::gen_u64(rng, 0, u64::MAX) },
155 (FieldTypeKind::U128, Some(c)) => {
157 let (lo, hi) = c.exclusive_bounds("e! { u128 });
158 quote! { crucible_fuzzer::gen_u128(rng, #lo, #hi) }
159 }
160 (FieldTypeKind::U128, None) => quote! { crucible_fuzzer::gen_u128(rng, 0, u128::MAX) },
161 (FieldTypeKind::U8 | FieldTypeKind::U16 | FieldTypeKind::U32, Some(c)) => {
163 let (lo, hi) = c.exclusive_bounds("e! { u64 });
164 let ty = inner_ty.expect("small unsigned needs inner_ty");
165 quote! { crucible_fuzzer::gen_u64(rng, #lo, #hi) as #ty }
166 }
167 (FieldTypeKind::U8 | FieldTypeKind::U16 | FieldTypeKind::U32, None) => {
168 let ty = inner_ty.expect("small unsigned needs inner_ty");
169 quote! { crucible_fuzzer::gen_u64(rng, 0, (#ty::MAX as u64) + 1) as #ty }
170 }
171 (FieldTypeKind::I64, Some(c)) => {
173 let (lo, hi) = c.exclusive_bounds("e! { i64 });
174 quote! { crucible_fuzzer::gen_i64(rng, #lo, #hi) }
175 }
176 (FieldTypeKind::I64, None) => quote! { crucible_fuzzer::gen_i64(rng, i64::MIN, i64::MAX) },
177 (FieldTypeKind::I128, Some(c)) => {
179 let (lo, hi) = c.exclusive_bounds("e! { i128 });
180 quote! { crucible_fuzzer::gen_i128(rng, #lo, #hi) }
181 }
182 (FieldTypeKind::I128, None) => {
183 quote! { crucible_fuzzer::gen_i128(rng, i128::MIN, i128::MAX) }
184 }
185 (FieldTypeKind::I8 | FieldTypeKind::I16 | FieldTypeKind::I32, Some(c)) => {
187 let (lo, hi) = c.exclusive_bounds("e! { i64 });
188 let ty = inner_ty.expect("small signed needs inner_ty");
189 quote! { crucible_fuzzer::gen_i64(rng, #lo, #hi) as #ty }
190 }
191 (FieldTypeKind::I8 | FieldTypeKind::I16 | FieldTypeKind::I32, None) => {
192 let ty = inner_ty.expect("small signed needs inner_ty");
193 quote! { crucible_fuzzer::gen_i64(rng, #ty::MIN as i64, (#ty::MAX as i64) + 1) as #ty }
194 }
195 (FieldTypeKind::Usize, Some(c)) => {
197 let (lo, hi) = c.exclusive_bounds("e! { usize });
198 quote! { crucible_fuzzer::gen_usize(rng, #lo, #hi) }
199 }
200 (FieldTypeKind::Usize, None) => quote! { crucible_fuzzer::gen_usize(rng, 0, usize::MAX) },
201 (FieldTypeKind::Bool, _) => quote! { crucible_fuzzer::rand_below(rng, 2) == 1 },
203 (FieldTypeKind::Option(_), _) | (FieldTypeKind::Vec(_), _) => {
204 unreachable!("handled at top level")
205 }
206 }
207}
208
209fn gen_inner_mutate_stmt(
212 inner_kind: &FieldTypeKind,
213 inner_ty: Option<&Type>,
214 ref_tok: &proc_macro2::TokenStream,
215 constraint: Option<&RangeConstraint>,
216) -> proc_macro2::TokenStream {
217 match (inner_kind, constraint) {
218 (FieldTypeKind::U64, Some(c)) => {
220 let (lo, hi) = c.exclusive_bounds("e! { u64 });
221 quote! { crucible_fuzzer::mutate_u64(#ref_tok, #lo, #hi, rng); }
222 }
223 (FieldTypeKind::U64, None) => {
224 quote! { crucible_fuzzer::mutate_u64(#ref_tok, 0, u64::MAX, rng); }
225 }
226 (FieldTypeKind::U128, Some(c)) => {
228 let (lo, hi) = c.exclusive_bounds("e! { u128 });
229 quote! { crucible_fuzzer::mutate_u128(#ref_tok, #lo, #hi, rng); }
230 }
231 (FieldTypeKind::U128, None) => {
232 quote! { crucible_fuzzer::mutate_u128(#ref_tok, 0, u128::MAX, rng); }
233 }
234 (FieldTypeKind::U8 | FieldTypeKind::U16 | FieldTypeKind::U32, c) => {
236 let ty = inner_ty.expect("small unsigned needs inner_ty");
237 let (lo, hi) = match c {
238 Some(c) => c.exclusive_bounds("e! { u64 }),
239 None => (quote! { 0u64 }, quote! { (#ty::MAX as u64) + 1 }),
240 };
241 quote! {
242 {
243 let mut __w = *#ref_tok as u64;
244 crucible_fuzzer::mutate_u64(&mut __w, #lo, #hi, rng);
245 *#ref_tok = __w as #ty;
246 }
247 }
248 }
249 (FieldTypeKind::I64, Some(c)) => {
251 let (lo, hi) = c.exclusive_bounds("e! { i64 });
252 quote! { crucible_fuzzer::mutate_i64(#ref_tok, #lo, #hi, rng); }
253 }
254 (FieldTypeKind::I64, None) => {
255 quote! { crucible_fuzzer::mutate_i64(#ref_tok, i64::MIN, i64::MAX, rng); }
256 }
257 (FieldTypeKind::I128, Some(c)) => {
259 let (lo, hi) = c.exclusive_bounds("e! { i128 });
260 quote! { crucible_fuzzer::mutate_i128(#ref_tok, #lo, #hi, rng); }
261 }
262 (FieldTypeKind::I128, None) => {
263 quote! { crucible_fuzzer::mutate_i128(#ref_tok, i128::MIN, i128::MAX, rng); }
264 }
265 (FieldTypeKind::I8 | FieldTypeKind::I16 | FieldTypeKind::I32, c) => {
267 let ty = inner_ty.expect("small signed needs inner_ty");
268 let (lo, hi) = match c {
269 Some(c) => c.exclusive_bounds("e! { i64 }),
270 None => (quote! { #ty::MIN as i64 }, quote! { (#ty::MAX as i64) + 1 }),
271 };
272 quote! {
273 {
274 let mut __w = *#ref_tok as i64;
275 crucible_fuzzer::mutate_i64(&mut __w, #lo, #hi, rng);
276 *#ref_tok = __w as #ty;
277 }
278 }
279 }
280 (FieldTypeKind::Usize, Some(c)) => {
282 let (lo, hi) = c.exclusive_bounds("e! { usize });
283 quote! { crucible_fuzzer::mutate_usize(#ref_tok, #lo, #hi, rng); }
284 }
285 (FieldTypeKind::Usize, None) => {
286 quote! { crucible_fuzzer::mutate_usize(#ref_tok, 0, usize::MAX, rng); }
287 }
288 (FieldTypeKind::Bool, _) => {
290 quote! { crucible_fuzzer::mutate_bool(#ref_tok, rng); }
291 }
292 (FieldTypeKind::Option(_), _) | (FieldTypeKind::Vec(_), _) => {
293 unreachable!("handled at top level")
294 }
295 }
296}
297
298fn gen_random_field_code(
302 name: &Ident,
303 ty: &Type,
304 constraint: Option<&RangeConstraint>,
305 max_len: Option<usize>,
306) -> proc_macro2::TokenStream {
307 let kind = classify_field_type(ty);
308
309 if let FieldTypeKind::Vec(ref inner_kind) = kind {
310 let ml = max_len.unwrap_or(8);
311 let inner_ty = extract_vec_inner(ty);
312 let inner_expr = gen_inner_random_expr(inner_kind, inner_ty, constraint);
313 return quote! {
314 #name: {
315 let __len = crucible_fuzzer::rand_below(rng, #ml + 1);
316 (0..__len).map(|_| #inner_expr).collect::<Vec<_>>()
317 },
318 };
319 }
320
321 let (inner_kind, is_option) = match &kind {
322 FieldTypeKind::Option(inner) => (inner.as_ref(), true),
323 other => (other, false),
324 };
325 let inner_ty = if is_option {
326 extract_option_inner(ty)
327 } else {
328 Some(ty)
329 };
330 let inner_expr = gen_inner_random_expr(inner_kind, inner_ty, constraint);
331
332 if is_option {
333 quote! { #name: if crucible_fuzzer::rand_below(rng, 4) == 0 { None } else { Some(#inner_expr) }, }
334 } else {
335 quote! { #name: #inner_expr, }
336 }
337}
338
339fn gen_mutate_field_code(
341 field_idx: usize,
342 name: &Ident,
343 ty: &Type,
344 constraint: Option<&RangeConstraint>,
345 max_len: Option<usize>,
346) -> proc_macro2::TokenStream {
347 let kind = classify_field_type(ty);
348
349 if let FieldTypeKind::Vec(ref inner_kind) = kind {
350 let ml = max_len.unwrap_or(8);
351 let inner_ty = extract_vec_inner(ty);
352 let inner_random_expr = gen_inner_random_expr(inner_kind, inner_ty, constraint);
353 let elem_ref = quote! { &mut #name[__idx] };
354 let inner_mutate = gen_inner_mutate_stmt(inner_kind, inner_ty, &elem_ref, constraint);
355 return quote! {
356 #field_idx => {
357 if crucible_fuzzer::rand_below(rng, 100) < 20 {
358 if #name.is_empty() || crucible_fuzzer::rand_below(rng, 2) == 0 {
359 if #name.len() < #ml {
360 #name.push(#inner_random_expr);
361 }
362 } else {
363 let __idx = crucible_fuzzer::rand_below(rng, #name.len());
364 #name.remove(__idx);
365 }
366 } else if !#name.is_empty() {
367 let __idx = crucible_fuzzer::rand_below(rng, #name.len());
368 #inner_mutate
369 }
370 },
371 };
372 }
373
374 let (inner_kind, is_option) = match &kind {
375 FieldTypeKind::Option(inner) => (inner.as_ref(), true),
376 other => (other, false),
377 };
378
379 if is_option {
380 let inner_ty = extract_option_inner(ty);
381 let random_expr = gen_inner_random_expr(inner_kind, inner_ty, constraint);
382 let inner_ref = quote! { __inner };
383 let mutate_stmt = gen_inner_mutate_stmt(inner_kind, inner_ty, &inner_ref, constraint);
384 quote! {
385 #field_idx => {
386 if crucible_fuzzer::rand_below(rng, 100) < 15 {
387 if #name.is_some() {
388 *#name = None;
389 } else {
390 *#name = Some(#random_expr);
391 }
392 } else if let Some(ref mut __inner) = #name {
393 #mutate_stmt
394 }
395 },
396 }
397 } else {
398 let ref_tok = quote! { #name };
399 let mutate_stmt = gen_inner_mutate_stmt(inner_kind, Some(ty), &ref_tok, constraint);
400 quote! { #field_idx => { #mutate_stmt }, }
401 }
402}
403
404fn gen_serialize_field_code(
406 name: &Ident,
407 ty: &Type,
408 max_len: Option<usize>,
409) -> proc_macro2::TokenStream {
410 let kind = classify_field_type(ty);
411
412 if let FieldTypeKind::Vec(ref inner_kind) = kind {
413 let ml = max_len.unwrap_or(8);
414 let (elem_bytes, pad_bytes) = match inner_kind.as_ref() {
415 FieldTypeKind::U128 => (
416 quote! { (*__item as u128).to_le_bytes() },
417 quote! { 0u128.to_le_bytes() },
418 ),
419 FieldTypeKind::I128 => (
420 quote! { (*__item as u128).to_le_bytes() },
421 quote! { 0u128.to_le_bytes() },
422 ),
423 FieldTypeKind::U8 | FieldTypeKind::U16 | FieldTypeKind::U32 | FieldTypeKind::U64 => (
424 quote! { (*__item as u64).to_le_bytes() },
425 quote! { 0u64.to_le_bytes() },
426 ),
427 FieldTypeKind::Usize => (
428 quote! { (*__item as u64).to_le_bytes() },
429 quote! { 0u64.to_le_bytes() },
430 ),
431 FieldTypeKind::Bool => (
432 quote! { (if *__item { 1u64 } else { 0u64 }).to_le_bytes() },
433 quote! { 0u64.to_le_bytes() },
434 ),
435 FieldTypeKind::I8 | FieldTypeKind::I16 | FieldTypeKind::I32 | FieldTypeKind::I64 => (
436 quote! { (*__item as u64).to_le_bytes() },
437 quote! { 0u64.to_le_bytes() },
438 ),
439 _ => unreachable!(),
440 };
441 return quote! {
442 buf.extend_from_slice(&(#name.len() as u64).to_le_bytes());
443 for __item in #name.iter() {
444 buf.extend_from_slice(&#elem_bytes);
445 }
446 for _ in #name.len()..#ml {
447 buf.extend_from_slice(&#pad_bytes);
448 }
449 };
450 }
451
452 let (inner_kind, is_option) = match &kind {
453 FieldTypeKind::Option(inner) => (inner.as_ref(), true),
454 other => (other, false),
455 };
456
457 let to_bytes = |val_tok: proc_macro2::TokenStream,
459 kind: &FieldTypeKind|
460 -> proc_macro2::TokenStream {
461 match kind {
462 FieldTypeKind::U128 => quote! { (#val_tok as u128).to_le_bytes() },
463 FieldTypeKind::I128 => quote! { (#val_tok as u128).to_le_bytes() },
464 FieldTypeKind::U8 | FieldTypeKind::U16 | FieldTypeKind::U32 | FieldTypeKind::U64 => {
465 quote! { (#val_tok as u64).to_le_bytes() }
466 }
467 FieldTypeKind::Usize => quote! { (#val_tok as u64).to_le_bytes() },
468 FieldTypeKind::Bool => quote! { (if #val_tok { 1u64 } else { 0u64 }).to_le_bytes() },
469 FieldTypeKind::I8 | FieldTypeKind::I16 | FieldTypeKind::I32 | FieldTypeKind::I64 => {
470 quote! { (#val_tok as u64).to_le_bytes() }
471 }
472 FieldTypeKind::Option(_) | FieldTypeKind::Vec(_) => unreachable!(),
473 }
474 };
475
476 if is_option {
477 let some_bytes = to_bytes(quote! { *__v }, inner_kind);
478 let none_bytes = match inner_kind {
479 FieldTypeKind::U128 | FieldTypeKind::I128 => quote! { u128::MAX.to_le_bytes() },
480 _ => quote! { u64::MAX.to_le_bytes() },
481 };
482 quote! {
483 match #name {
484 Some(__v) => buf.extend_from_slice(&#some_bytes),
485 None => buf.extend_from_slice(&#none_bytes),
486 }
487 }
488 } else {
489 let bytes = to_bytes(quote! { *#name }, inner_kind);
490 quote! { buf.extend_from_slice(&#bytes); }
491 }
492}
493
494fn gen_deserialize_field_code(
496 name: &Ident,
497 ty: &Type,
498 max_len: Option<usize>,
499) -> proc_macro2::TokenStream {
500 let kind = classify_field_type(ty);
501
502 if let FieldTypeKind::Vec(ref inner_kind) = kind {
503 let ml = max_len.unwrap_or(8);
504 let is_128 = matches!(
505 inner_kind.as_ref(),
506 FieldTypeKind::U128 | FieldTypeKind::I128
507 );
508 let elem_size: usize = if is_128 { 16 } else { 8 };
509 let (read_raw, raw_to_val) = if is_128 {
510 let inner_ty = extract_vec_inner(ty).expect("Vec<T> inner type");
511 (
512 quote! { u128::from_le_bytes(bytes[*cursor..*cursor + 16].try_into().ok()?) },
513 quote! { __raw as #inner_ty },
514 )
515 } else {
516 match inner_kind.as_ref() {
517 FieldTypeKind::U8
518 | FieldTypeKind::U16
519 | FieldTypeKind::U32
520 | FieldTypeKind::U64 => {
521 let inner_ty = extract_vec_inner(ty).expect("Vec<T> inner type");
522 (
523 quote! { u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?) },
524 quote! { __raw as #inner_ty },
525 )
526 }
527 FieldTypeKind::Usize => (
528 quote! { u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?) },
529 quote! { __raw as usize },
530 ),
531 FieldTypeKind::Bool => (
532 quote! { u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?) },
533 quote! { __raw != 0 },
534 ),
535 FieldTypeKind::I8
536 | FieldTypeKind::I16
537 | FieldTypeKind::I32
538 | FieldTypeKind::I64 => {
539 let inner_ty = extract_vec_inner(ty).expect("Vec<T> inner type");
540 (
541 quote! { u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?) },
542 quote! { __raw as #inner_ty },
543 )
544 }
545 _ => unreachable!(),
546 }
547 };
548 return quote! {
549 let __vec_len = (u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?) as usize).min(#ml);
550 *cursor += 8;
551 let mut #name = Vec::with_capacity(__vec_len);
552 for __i in 0usize..#ml {
553 let __raw = #read_raw;
554 if __i < __vec_len {
555 #name.push(#raw_to_val);
556 }
557 *cursor += #elem_size;
558 }
559 };
560 }
561
562 let (inner_kind, is_option) = match &kind {
563 FieldTypeKind::Option(inner) => (inner.as_ref(), true),
564 other => (other, false),
565 };
566
567 if is_option {
568 match inner_kind {
569 FieldTypeKind::U128 | FieldTypeKind::I128 => {
570 let inner_ty = extract_option_inner(ty).unwrap_or(ty);
571 quote! {
572 let __raw = u128::from_le_bytes(bytes[*cursor..*cursor + 16].try_into().ok()?);
573 let #name = if __raw == u128::MAX { None } else { Some(__raw as #inner_ty) };
574 *cursor += 16;
575 }
576 }
577 _ => {
578 let raw_to_val = match inner_kind {
579 FieldTypeKind::U8
580 | FieldTypeKind::U16
581 | FieldTypeKind::U32
582 | FieldTypeKind::U64 => {
583 let inner_ty = extract_option_inner(ty).unwrap_or(ty);
584 quote! { __raw as #inner_ty }
585 }
586 FieldTypeKind::Usize => quote! { __raw as usize },
587 FieldTypeKind::Bool => quote! { __raw != 0 },
588 FieldTypeKind::I8
589 | FieldTypeKind::I16
590 | FieldTypeKind::I32
591 | FieldTypeKind::I64 => {
592 let inner_ty = extract_option_inner(ty).unwrap_or(ty);
593 quote! { __raw as #inner_ty }
594 }
595 FieldTypeKind::Option(_) | FieldTypeKind::Vec(_) => unreachable!(),
596 FieldTypeKind::U128 | FieldTypeKind::I128 => unreachable!("handled above"),
597 };
598 quote! {
599 let __raw = u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?);
600 let #name = if __raw == u64::MAX { None } else { Some(#raw_to_val) };
601 *cursor += 8;
602 }
603 }
604 }
605 } else {
606 match inner_kind {
607 FieldTypeKind::U128 => quote! {
608 let #name = u128::from_le_bytes(bytes[*cursor..*cursor + 16].try_into().ok()?) as #ty;
609 *cursor += 16;
610 },
611 FieldTypeKind::I128 => quote! {
612 let #name = u128::from_le_bytes(bytes[*cursor..*cursor + 16].try_into().ok()?) as #ty;
613 *cursor += 16;
614 },
615 FieldTypeKind::U8 | FieldTypeKind::U16 | FieldTypeKind::U32 | FieldTypeKind::U64 => {
616 quote! {
617 let #name = u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?) as #ty;
618 *cursor += 8;
619 }
620 }
621 FieldTypeKind::Usize => quote! {
622 let #name = u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?) as usize;
623 *cursor += 8;
624 },
625 FieldTypeKind::Bool => quote! {
626 let #name = u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?) != 0;
627 *cursor += 8;
628 },
629 FieldTypeKind::I8 | FieldTypeKind::I16 | FieldTypeKind::I32 | FieldTypeKind::I64 => {
630 quote! {
631 let #name = u64::from_le_bytes(bytes[*cursor..*cursor + 8].try_into().ok()?) as #ty;
632 *cursor += 8;
633 }
634 }
635 FieldTypeKind::Option(_) | FieldTypeKind::Vec(_) => unreachable!(),
636 }
637 }
638}
639
640fn gen_from_json_field_code(name: &Ident, ty: &Type) -> proc_macro2::TokenStream {
643 let name_str = name.to_string();
644 let kind = classify_field_type(ty);
645 gen_from_json_kind(name, &name_str, ty, &kind)
646}
647
648fn gen_from_json_kind(
649 name: &Ident,
650 name_str: &str,
651 ty: &Type,
652 kind: &FieldTypeKind,
653) -> proc_macro2::TokenStream {
654 match kind {
655 FieldTypeKind::U64 => quote! { let #name = __params.get(#name_str)?.as_u64()?; },
656 FieldTypeKind::U8 => quote! { let #name = __params.get(#name_str)?.as_u64()? as u8; },
657 FieldTypeKind::U16 => quote! { let #name = __params.get(#name_str)?.as_u64()? as u16; },
658 FieldTypeKind::U32 => quote! { let #name = __params.get(#name_str)?.as_u64()? as u32; },
659 FieldTypeKind::U128 => quote! { let #name = __params.get(#name_str)?.as_u64()? as u128; },
660 FieldTypeKind::Usize => quote! { let #name = __params.get(#name_str)?.as_u64()? as usize; },
661 FieldTypeKind::I64 => quote! { let #name = __params.get(#name_str)?.as_i64()?; },
662 FieldTypeKind::I8 => quote! { let #name = __params.get(#name_str)?.as_i64()? as i8; },
663 FieldTypeKind::I16 => quote! { let #name = __params.get(#name_str)?.as_i64()? as i16; },
664 FieldTypeKind::I32 => quote! { let #name = __params.get(#name_str)?.as_i64()? as i32; },
665 FieldTypeKind::I128 => quote! { let #name = __params.get(#name_str)?.as_i64()? as i128; },
666 FieldTypeKind::Bool => quote! { let #name = __params.get(#name_str)?.as_bool()?; },
667 FieldTypeKind::Option(inner) => {
668 let inner_ty = extract_option_inner(ty).unwrap_or(ty);
669 let inner_extract = match inner.as_ref() {
670 FieldTypeKind::U64 => quote! { __v.as_u64()? },
671 FieldTypeKind::U8 => quote! { __v.as_u64()? as u8 },
672 FieldTypeKind::U16 => quote! { __v.as_u64()? as u16 },
673 FieldTypeKind::U32 => quote! { __v.as_u64()? as u32 },
674 FieldTypeKind::U128 => quote! { __v.as_u64()? as u128 },
675 FieldTypeKind::Usize => quote! { __v.as_u64()? as usize },
676 FieldTypeKind::I64 => quote! { __v.as_i64()? },
677 FieldTypeKind::I8 => quote! { __v.as_i64()? as i8 },
678 FieldTypeKind::I16 => quote! { __v.as_i64()? as i16 },
679 FieldTypeKind::I32 => quote! { __v.as_i64()? as i32 },
680 FieldTypeKind::I128 => quote! { __v.as_i64()? as i128 },
681 FieldTypeKind::Bool => quote! { __v.as_bool()? },
682 _ => quote! { __v.as_u64()? as #inner_ty },
683 };
684 quote! {
685 let #name: Option<#inner_ty> = match __params.get(#name_str) {
686 Some(__v) if __v.is_null() => None,
687 Some(__v) => Some(#inner_extract),
688 None => None,
689 };
690 }
691 }
692 FieldTypeKind::Vec(inner) => {
693 let inner_ty = extract_vec_inner(ty).unwrap_or(ty);
694 let elem_extract = match inner.as_ref() {
695 FieldTypeKind::U64 => quote! { __e.as_u64()? },
696 FieldTypeKind::U8 => quote! { __e.as_u64()? as u8 },
697 FieldTypeKind::U16 => quote! { __e.as_u64()? as u16 },
698 FieldTypeKind::U32 => quote! { __e.as_u64()? as u32 },
699 FieldTypeKind::U128 => quote! { __e.as_u64()? as u128 },
700 FieldTypeKind::Usize => quote! { __e.as_u64()? as usize },
701 FieldTypeKind::I64 => quote! { __e.as_i64()? },
702 FieldTypeKind::I8 => quote! { __e.as_i64()? as i8 },
703 FieldTypeKind::I16 => quote! { __e.as_i64()? as i16 },
704 FieldTypeKind::I32 => quote! { __e.as_i64()? as i32 },
705 FieldTypeKind::I128 => quote! { __e.as_i64()? as i128 },
706 FieldTypeKind::Bool => quote! { __e.as_bool()? },
707 _ => quote! { __e.as_u64()? as #inner_ty },
708 };
709 quote! {
710 let #name: Vec<#inner_ty> = {
711 let __arr = __params.get(#name_str)?.as_array()?;
712 let __items: Option<Vec<#inner_ty>> = __arr.iter().map(|__e| Some(#elem_extract)).collect();
713 __items?
714 };
715 }
716 }
717 }
718}
719
720fn gen_constraint_code(
722 field_name: &Ident,
723 field_type: &Type,
724 constraint: &RangeConstraint,
725) -> proc_macro2::TokenStream {
726 if let Some(inner_ty) = extract_vec_inner(field_type) {
727 constraint.generate_vec_constraint_expr(field_name, &inner_ty)
728 } else if let Some(inner_ty) = extract_option_inner(field_type) {
729 constraint.generate_option_constraint_expr(field_name, &inner_ty)
730 } else {
731 constraint.generate_constraint_expr(field_name, field_type)
732 }
733}
734
735#[proc_macro_attribute]
736pub fn fuzz_fixture(_args: TokenStream, item: TokenStream) -> TokenStream {
737 let mut input = parse_macro_input!(item as ItemImpl);
738
739 let fixture_type = &input.self_ty;
740
741 let fixture_name = match &**fixture_type {
742 Type::Path(type_path) => type_path
743 .path
744 .segments
745 .last()
746 .map(|s| s.ident.clone())
747 .expect("Expected a type name"),
748 _ => panic!("Expected a simple type path"),
749 };
750
751 let mut actions = Vec::new();
753 let mut constraints: HashMap<(String, String), RangeConstraint> = HashMap::new();
754 let mut max_lens: HashMap<(String, String), MaxLenConstraint> = HashMap::new();
755 let mut has_after_action = false;
756
757 let mut new_items: Vec<ImplItem> = Vec::new();
762 let manifest_dir_str = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
763 let src_dir_path = std::path::PathBuf::from(&manifest_dir_str).join("src");
764 for item in input.items.drain(..) {
765 if let ImplItem::Macro(ref mac) = item {
766 if mac.mac.path.is_ident("include") {
767 if let Ok(lit) = mac.mac.parse_body::<syn::LitStr>() {
768 let file_path = src_dir_path.join(lit.value());
769 if let Ok(content) = std::fs::read_to_string(&file_path) {
770 let wrapped = format!("impl Dummy {{ {} }}", content);
771 if let Ok(parsed) = syn::parse_str::<ItemImpl>(&wrapped) {
772 for inner in parsed.items {
773 new_items.push(inner);
774 }
775 continue;
776 }
777 }
778 }
779 }
780 }
781 new_items.push(item);
782 }
783 input.items = new_items;
784
785 fn process_method(
787 method: &mut syn::ImplItemFn,
788 actions: &mut Vec<(Ident, Ident, Vec<(Ident, Box<Type>)>)>,
789 constraints: &mut HashMap<(String, String), RangeConstraint>,
790 max_lens: &mut HashMap<(String, String), MaxLenConstraint>,
791 has_after_action: &mut bool,
792 ) {
793 let method_name = method.sig.ident.to_string();
794
795 if method_name == "after_action" {
796 *has_after_action = true;
797 }
798
799 if method_name.starts_with("action_") {
800 let action_name = &method_name[7..];
801 let action_ident = format_ident!("{}", to_pascal_case(action_name));
802
803 let mut params = Vec::new();
804 for arg in &mut method.sig.inputs {
805 if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = arg {
806 if let syn::Pat::Ident(pat_ident) = &**pat {
807 if pat_ident.ident != "self" {
808 if let Some(range_attr) =
809 attrs.iter().find(|a| a.path().is_ident("range"))
810 {
811 if let Ok(constraint) = RangeConstraint::from_attr(range_attr) {
812 constraints.insert(
813 (action_ident.to_string(), pat_ident.ident.to_string()),
814 constraint,
815 );
816 }
817 }
818 if let Some(ml_attr) =
819 attrs.iter().find(|a| a.path().is_ident("max_len"))
820 {
821 if let Ok(ml) = MaxLenConstraint::from_attr(ml_attr) {
822 max_lens.insert(
823 (action_ident.to_string(), pat_ident.ident.to_string()),
824 ml,
825 );
826 }
827 }
828 attrs.retain(|a| {
829 !a.path().is_ident("range") && !a.path().is_ident("max_len")
830 });
831 params.push((pat_ident.ident.clone(), ty.clone()));
832 }
833 }
834 }
835 }
836
837 actions.push((action_ident, method.sig.ident.clone(), params));
838 }
839 }
840
841 for item in &mut input.items {
842 if let ImplItem::Fn(method) = item {
843 process_method(
844 method,
845 &mut actions,
846 &mut constraints,
847 &mut max_lens,
848 &mut has_after_action,
849 );
850 }
851 }
852
853 if actions.is_empty() {
856 panic!("No action_* methods found in impl block. Methods must be named action_something()");
857 }
858
859 let enum_name = format_ident!("{}Actions", fixture_name);
860 let mod_name = format_ident!("__{}_fuzz", to_snake_case(&fixture_name.to_string()));
861
862 let enum_variants = actions.iter().map(|(action_name, _, params)| {
864 if params.is_empty() {
865 quote! { #action_name }
866 } else {
867 let fields = params.iter().map(|(name, ty)| {
868 quote! { #name: #ty }
869 });
870 quote! { #action_name { #(#fields),* } }
871 }
872 });
873
874 let action_name_arms = actions.iter().map(|(action_name, _, params)| {
876 let action_str = to_snake_case(&action_name.to_string());
877 if params.is_empty() {
878 quote! {
879 #enum_name::#action_name => #action_str,
880 }
881 } else {
882 quote! {
883 #enum_name::#action_name { .. } => #action_str,
884 }
885 }
886 });
887
888 let to_json_arms = actions.iter().map(|(action_name, _, params)| {
890 if params.is_empty() {
891 quote! {
892 #enum_name::#action_name => crucible_test_context::serde_json::json!({}),
893 }
894 } else {
895 let field_names: Vec<_> = params.iter().map(|(name, _)| name).collect();
896 let json_fields = params.iter().map(|(name, _)| {
897 let name_str = name.to_string();
898 quote! { #name_str: #name }
899 });
900 quote! {
901 #enum_name::#action_name { #(#field_names),* } => crucible_test_context::serde_json::json!({
902 #(#json_fields),*
903 }),
904 }
905 }
906 });
907
908 let dispatch_arms = actions.iter().map(|(action_name, method_name, params)| {
909 if params.is_empty() {
910 quote! {
911 #enum_name::#action_name => self.#method_name().into_success(),
912 }
913 } else {
914 let param_names = params.iter().map(|(name, _)| name);
915 let param_names_in_call = params.iter().map(|(name, _)| name);
916 quote! {
917 #enum_name::#action_name { #(#param_names),* } => {
918 self.#method_name(#(#param_names_in_call),*).into_success()
919 }
920 }
921 }
922 });
923
924 let constrain_arms: Vec<_> = actions
926 .iter()
927 .map(|(action_name, _, params)| {
928 let field_constraints: Vec<_> = params
930 .iter()
931 .filter_map(|(field_name, field_type)| {
932 constraints
933 .get(&(action_name.to_string(), field_name.to_string()))
934 .map(|constraint| gen_constraint_code(field_name, field_type, constraint))
935 })
936 .collect();
937
938 if field_constraints.is_empty() {
939 quote! { #enum_name::#action_name { .. } => {} }
940 } else if params.is_empty() {
941 quote! { #enum_name::#action_name => {} }
942 } else {
943 let field_names = params.iter().map(|(name, _)| name);
944 quote! {
945 #enum_name::#action_name { #(#field_names),* } => {
946 #(#field_constraints)*
947 }
948 }
949 }
950 })
951 .collect();
952
953 let num_actions = actions.len();
955
956 let mut random_variant_arms = Vec::new();
957 let mut mutate_arms_fuzz = Vec::new();
958 let mut variant_index_arms = Vec::new();
959 let mut serialize_arms = Vec::new();
960 let mut deserialize_arms = Vec::new();
961 let mut fuzz_action_name_arms = Vec::new();
962 let mut field_byte_count_arms = Vec::new();
963 let mut from_json_arms = Vec::new();
964
965 for (idx, (action_name, _, params)) in actions.iter().enumerate() {
966 let action_str = to_snake_case(&action_name.to_string());
967
968 if params.is_empty() {
969 random_variant_arms.push(quote! { #idx => Self::#action_name, });
970 mutate_arms_fuzz.push(quote! { Self::#action_name => {}, });
971 variant_index_arms.push(quote! { Self::#action_name => #idx, });
972 serialize_arms.push(quote! { Self::#action_name => {}, });
973 deserialize_arms.push(quote! { #idx => Some(Self::#action_name), });
974 fuzz_action_name_arms.push(quote! { Self::#action_name => #action_str, });
975 field_byte_count_arms.push(quote! { #idx => 0, });
976 from_json_arms.push(quote! { #action_str => Some(Self::#action_name), });
977 } else {
978 let field_names: Vec<_> = params.iter().map(|(name, _)| name.clone()).collect();
979 let num_fields = params.len();
980
981 let field_sizes: Vec<usize> = params
983 .iter()
984 .map(|(name, ty)| {
985 let kind = classify_field_type(ty);
986 let ml = max_lens
987 .get(&(action_name.to_string(), name.to_string()))
988 .map(|m| m.max_len);
989 field_byte_size(&kind, ml)
990 })
991 .collect();
992 let total_bytes: usize = field_sizes.iter().sum();
993
994 let random_fields: Vec<_> = params
995 .iter()
996 .map(|(name, ty)| {
997 let c = constraints.get(&(action_name.to_string(), name.to_string()));
998 let ml = max_lens
999 .get(&(action_name.to_string(), name.to_string()))
1000 .map(|m| m.max_len);
1001 gen_random_field_code(name, ty, c, ml)
1002 })
1003 .collect();
1004
1005 let mutate_field_arms: Vec<_> = params
1006 .iter()
1007 .enumerate()
1008 .map(|(fi, (name, ty))| {
1009 let c = constraints.get(&(action_name.to_string(), name.to_string()));
1010 let ml = max_lens
1011 .get(&(action_name.to_string(), name.to_string()))
1012 .map(|m| m.max_len);
1013 gen_mutate_field_code(fi, name, ty, c, ml)
1014 })
1015 .collect();
1016
1017 let ser_fields: Vec<_> = params
1018 .iter()
1019 .map(|(name, ty)| {
1020 let ml = max_lens
1021 .get(&(action_name.to_string(), name.to_string()))
1022 .map(|m| m.max_len);
1023 gen_serialize_field_code(name, ty, ml)
1024 })
1025 .collect();
1026
1027 let deser_fields: Vec<_> = params
1028 .iter()
1029 .map(|(name, ty)| {
1030 let ml = max_lens
1031 .get(&(action_name.to_string(), name.to_string()))
1032 .map(|m| m.max_len);
1033 gen_deserialize_field_code(name, ty, ml)
1034 })
1035 .collect();
1036
1037 random_variant_arms.push(quote! {
1038 #idx => Self::#action_name { #(#random_fields)* },
1039 });
1040
1041 mutate_arms_fuzz.push(quote! {
1042 Self::#action_name { #(#field_names),* } => {
1043 let num_mutations = 1 + crucible_fuzzer::rand_below(rng, (#num_fields).min(3));
1044 for _ in 0..num_mutations {
1045 match crucible_fuzzer::rand_below(rng, #num_fields) {
1046 #(#mutate_field_arms)*
1047 _ => {}
1048 }
1049 }
1050 },
1051 });
1052
1053 variant_index_arms.push(quote! { Self::#action_name { .. } => #idx, });
1054
1055 serialize_arms.push(quote! {
1056 Self::#action_name { #(#field_names),* } => {
1057 #(#ser_fields)*
1058 },
1059 });
1060
1061 deserialize_arms.push(quote! {
1062 #idx => {
1063 if *cursor + #total_bytes > bytes.len() {
1064 return None;
1065 }
1066 #(#deser_fields)*
1067 Some(Self::#action_name { #(#field_names),* })
1068 },
1069 });
1070
1071 fuzz_action_name_arms.push(quote! { Self::#action_name { .. } => #action_str, });
1072
1073 field_byte_count_arms.push(quote! { #idx => #total_bytes, });
1074
1075 let from_json_fields: Vec<_> = params
1077 .iter()
1078 .map(|(name, ty)| gen_from_json_field_code(name, ty))
1079 .collect();
1080 from_json_arms.push(quote! {
1081 #action_str => {
1082 #(#from_json_fields)*
1083 Some(Self::#action_name { #(#field_names),* })
1084 },
1085 });
1086 }
1087 }
1088
1089 let generated = quote! {
1090 #input
1091
1092 #[doc(hidden)]
1093 pub mod #mod_name {
1094 use super::*;
1095 use arbitrary::Arbitrary;
1096
1097 #[derive(Arbitrary, Debug, Clone)]
1098 pub enum #enum_name {
1099 #(#enum_variants),*
1100 }
1101
1102 impl #enum_name {
1103 pub fn constrain_in_place(&mut self) {
1104 match self {
1105 #(#constrain_arms)*
1106 }
1107 }
1108
1109 pub fn action_name(&self) -> &'static str {
1111 match self {
1112 #(#action_name_arms)*
1113 }
1114 }
1115
1116 pub fn to_json_params(&self) -> crucible_test_context::serde_json::Value {
1118 match self {
1119 #(#to_json_arms)*
1120 }
1121 }
1122 }
1123
1124 impl crucible_fuzzer::FuzzAction for #enum_name {
1126 fn variant_count() -> usize {
1127 #num_actions
1128 }
1129
1130 fn random_variant<R: crucible_fuzzer::FuzzRand>(variant_idx: usize, rng: &mut R) -> Self {
1131 match variant_idx % #num_actions {
1132 #(#random_variant_arms)*
1133 _ => unreachable!(),
1134 }
1135 }
1136
1137 fn mutate<R: crucible_fuzzer::FuzzRand>(&mut self, rng: &mut R) {
1138 match self {
1139 #(#mutate_arms_fuzz)*
1140 }
1141 }
1142
1143 fn action_name(&self) -> &'static str {
1144 match self {
1145 #(#fuzz_action_name_arms)*
1146 }
1147 }
1148
1149 fn variant_index(&self) -> usize {
1150 match self {
1151 #(#variant_index_arms)*
1152 }
1153 }
1154
1155 fn serialize_fields(&self, buf: &mut Vec<u8>) {
1156 match self {
1157 #(#serialize_arms)*
1158 }
1159 }
1160
1161 fn deserialize_fields(variant_idx: usize, bytes: &[u8], cursor: &mut usize) -> Option<Self> {
1162 match variant_idx {
1163 #(#deserialize_arms)*
1164 _ => None,
1165 }
1166 }
1167
1168 fn field_byte_count(variant_idx: usize) -> usize {
1169 match variant_idx {
1170 #(#field_byte_count_arms)*
1171 _ => 0,
1172 }
1173 }
1174
1175 fn from_name_and_params(__name: &str, __params: &crucible_fuzzer::serde_json::Value) -> Option<Self> {
1176 match __name {
1177 #(#from_json_arms)*
1178 _ => None,
1179 }
1180 }
1181 }
1182
1183 impl #fixture_type {
1184 #[doc(hidden)]
1185 pub fn __dispatch_action(&mut self, action: #enum_name) -> bool {
1188 use crucible_test_context::IntoActionSuccess;
1189
1190 crucible_test_context::set_current_instruction(Some(action.action_name().to_string()));
1192
1193 let success = match action {
1195 #(#dispatch_arms)*
1196 };
1197
1198 crucible_test_context::set_current_instruction(None);
1200
1201 self.__maybe_after_action();
1203
1204 success
1205 }
1206
1207 #[doc(hidden)]
1208 pub fn __auto_flush(&mut self) {
1209 let _ = self.ctx.send_batch();
1210 }
1211 }
1212 }
1213 };
1214
1215 let after_action_impl = if has_after_action {
1217 quote! {
1218 impl #fixture_type {
1219 #[doc(hidden)]
1220 #[inline(always)]
1221 fn __maybe_after_action(&mut self) {
1222 self.after_action();
1223 }
1224 }
1225 }
1226 } else {
1227 quote! {
1228 impl #fixture_type {
1229 #[doc(hidden)]
1230 #[inline(always)]
1231 fn __maybe_after_action(&mut self) {
1232 }
1234 }
1235 }
1236 };
1237
1238 let fallback_main = if !FALLBACK_MAIN_EMITTED.swap(true, Ordering::SeqCst) {
1239 let features = read_cargo_features();
1240 if features.is_empty() {
1241 quote! {}
1242 } else {
1243 let feature_guards: Vec<_> = features
1244 .iter()
1245 .map(|f| {
1246 quote! { feature = #f }
1247 })
1248 .collect();
1249 quote! {
1250 #[cfg(not(any(#(#feature_guards),*)))]
1251 fn main() {
1252 eprintln!("No fuzz test selected. Build with --features <test_name>");
1253 std::process::exit(1);
1254 }
1255 }
1256 }
1257 } else {
1258 quote! {}
1259 };
1260
1261 let final_output = quote! {
1262 #generated
1263 #after_action_impl
1264 #fallback_main
1265 };
1266
1267 TokenStream::from(final_output)
1268}
1269
1270#[proc_macro_attribute]
1271pub fn invariant_test(args: TokenStream, item: TokenStream) -> TokenStream {
1272 let args_tokens = proc_macro2::TokenStream::from(args);
1274 if !args_tokens.is_empty() {
1275 return syn::Error::new_spanned(
1276 args_tokens,
1277 "invariant_test no longer accepts arguments. Structured mutation is now the default.",
1278 )
1279 .to_compile_error()
1280 .into();
1281 }
1282
1283 let input_fn = parse_macro_input!(item as ItemFn);
1284 let fn_name = &input_fn.sig.ident;
1285 let fn_body = &input_fn.block;
1286
1287 let fixture_param = input_fn
1289 .sig
1290 .inputs
1291 .first()
1292 .expect("invariant_test function must have a fixture parameter");
1293
1294 let FnArg::Typed(pat_type) = fixture_param else {
1295 return syn::Error::new_spanned(fixture_param, "Expected typed parameter")
1296 .to_compile_error()
1297 .into();
1298 };
1299
1300 let fixture_type = match &*pat_type.ty {
1302 Type::Reference(type_ref) => &*type_ref.elem,
1303 _ => {
1304 return syn::Error::new_spanned(
1305 &pat_type.ty,
1306 "Fixture parameter must be a reference (&mut FixtureType)",
1307 )
1308 .to_compile_error()
1309 .into();
1310 }
1311 };
1312
1313 let fixture_name = match fixture_type {
1314 Type::Path(type_path) => type_path
1315 .path
1316 .segments
1317 .last()
1318 .map(|s| s.ident.clone())
1319 .expect("Expected fixture type name"),
1320 _ => {
1321 return syn::Error::new_spanned(
1322 fixture_type,
1323 "Expected a simple type path for fixture",
1324 )
1325 .to_compile_error()
1326 .into();
1327 }
1328 };
1329
1330 let mod_name = format_ident!("__{}_fuzz", to_snake_case(&fixture_name.to_string()));
1331 let enum_name = format_ident!("{}Actions", fixture_name);
1332
1333 let test_name_str = fn_name.to_string();
1334
1335 let fuzz_attr = quote! { #[crucible_fuzz(structured)] };
1336
1337 let expanded = quote! {
1338 #fuzz_attr
1339 fn #fn_name(fixture: &mut #fixture_name, actions: Vec<#mod_name::#enum_name>) {
1340 let debug = std::env::var("FUZZ_DEBUG").is_ok();
1341 let capped_len = actions.len();
1342
1343 if debug {
1344 eprintln!("[FUZZ] Starting iteration with {} actions", capped_len);
1345 }
1346
1347 crucible_test_context::clear_iteration_state();
1349 crucible_test_context::set_total_actions(capped_len);
1351 crucible_test_context::set_current_test_name(#test_name_str);
1353 crucible_test_context::TOTAL_ACTION_VARIANTS.store(
1355 <#mod_name::#enum_name as crucible_fuzzer::FuzzAction>::variant_count(),
1356 std::sync::atomic::Ordering::Relaxed,
1357 );
1358
1359 let mut __executed_actions: Vec<#mod_name::#enum_name> = Vec::with_capacity(capped_len);
1363
1364 for (i, mut action) in actions.into_iter().enumerate() {
1365 action.constrain_in_place();
1366
1367 if debug {
1368 eprintln!("[FUZZ] Action {}: {:?}", i, action);
1369 }
1370
1371 let variant_idx = crucible_fuzzer::FuzzAction::variant_index(&action);
1372
1373 let success = fixture.__dispatch_action(action.clone());
1375
1376 if success {
1377 crucible_test_context::mark_variant_succeeded(variant_idx);
1378 }
1379
1380 crucible_test_context::push_action_record_lite(action.action_name(), success);
1382
1383 __executed_actions.push(action);
1385
1386 #fn_body
1387
1388 if crucible_test_context::is_debug_replay() {
1390 let __has_viol = crucible_test_context::has_violation();
1391 let __dirty_keys: Vec<_> = fixture.ctx.dirty_tracker.dirty_accounts().iter().copied().collect();
1392 let (__state_hash, __slot) = crucible_test_context::compute_svm_debug_hash(
1393 &fixture.ctx.svm, &__dirty_keys,
1394 );
1395 eprintln!("[REPLAY_DIAG] action={}/{} variant={} success={} violation={} slot={} hash={:016x}",
1396 i + 1, capped_len,
1397 __executed_actions.last().unwrap().action_name(),
1398 success, __has_viol, __slot, __state_hash);
1399 }
1400
1401 if crucible_test_context::has_violation() {
1403 for (j, a) in __executed_actions.iter().enumerate() {
1405 crucible_test_context::backfill_action_params(j, a.to_json_params());
1406 }
1407 crucible_test_context::set_violation_action_index(i);
1408 break;
1409 }
1410
1411 if !success && crucible_test_context::is_stateful_chain_mode() {
1414 break;
1415 }
1416 }
1417
1418 if !crucible_test_context::has_violation()
1422 && (std::env::var("FUZZ_INPUT_FILE").is_ok() || debug)
1423 {
1424 for (j, a) in __executed_actions.iter().enumerate() {
1425 crucible_test_context::backfill_action_params(j, a.to_json_params());
1426 }
1427 }
1428
1429 fixture.__auto_flush();
1430 }
1431 };
1432
1433 TokenStream::from(expanded)
1434}
1435
1436fn to_pascal_case(s: &str) -> String {
1437 s.split('_')
1438 .map(|word| {
1439 let mut chars = word.chars();
1440 match chars.next() {
1441 None => String::new(),
1442 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1443 }
1444 })
1445 .collect()
1446}
1447
1448fn to_snake_case(s: &str) -> String {
1449 let mut result = String::new();
1450 for (i, ch) in s.chars().enumerate() {
1451 if ch.is_uppercase() && i > 0 {
1452 result.push('_');
1453 }
1454 result.push(ch.to_lowercase().next().unwrap());
1455 }
1456 result
1457}
1458
1459fn parse_features_from_content(content: &str) -> Vec<String> {
1462 let mut features = Vec::new();
1463 let mut in_features = false;
1464 for line in content.lines() {
1465 let trimmed = line.trim();
1466 if trimmed == "[features]" {
1467 in_features = true;
1468 continue;
1469 }
1470 if in_features && trimmed.starts_with('[') {
1471 break;
1472 }
1473 if in_features && !trimmed.is_empty() && !trimmed.starts_with('#') {
1474 if let Some(eq_pos) = trimmed.find('=') {
1475 let name = trimmed[..eq_pos].trim();
1476 if !name.is_empty() && name != "default" {
1477 features.push(name.to_string());
1478 }
1479 }
1480 }
1481 }
1482 features
1483}
1484
1485#[cfg(test)]
1486mod tests {
1487 use super::*;
1488 use syn::Type;
1489
1490 fn parse_type(s: &str) -> Type {
1491 syn::parse_str::<Type>(s).unwrap()
1492 }
1493
1494 #[test]
1499 fn test_classify_u8() {
1500 assert!(matches!(
1501 classify_field_type(&parse_type("u8")),
1502 FieldTypeKind::U8
1503 ));
1504 }
1505
1506 #[test]
1507 fn test_classify_u64() {
1508 assert!(matches!(
1509 classify_field_type(&parse_type("u64")),
1510 FieldTypeKind::U64
1511 ));
1512 }
1513
1514 #[test]
1515 fn test_classify_u128() {
1516 assert!(matches!(
1517 classify_field_type(&parse_type("u128")),
1518 FieldTypeKind::U128
1519 ));
1520 }
1521
1522 #[test]
1523 fn test_classify_bool() {
1524 assert!(matches!(
1525 classify_field_type(&parse_type("bool")),
1526 FieldTypeKind::Bool
1527 ));
1528 }
1529
1530 #[test]
1531 fn test_classify_i64() {
1532 assert!(matches!(
1533 classify_field_type(&parse_type("i64")),
1534 FieldTypeKind::I64
1535 ));
1536 }
1537
1538 #[test]
1539 fn test_classify_usize() {
1540 assert!(matches!(
1541 classify_field_type(&parse_type("usize")),
1542 FieldTypeKind::Usize
1543 ));
1544 }
1545
1546 #[test]
1547 fn test_classify_vec_u64() {
1548 match classify_field_type(&parse_type("Vec<u64>")) {
1549 FieldTypeKind::Vec(inner) => assert!(matches!(*inner, FieldTypeKind::U64)),
1550 other => panic!(
1551 "Expected Vec(U64), got {:?}",
1552 std::mem::discriminant(&other)
1553 ),
1554 }
1555 }
1556
1557 #[test]
1558 fn test_classify_option_u64() {
1559 match classify_field_type(&parse_type("Option<u64>")) {
1560 FieldTypeKind::Option(inner) => assert!(matches!(*inner, FieldTypeKind::U64)),
1561 other => panic!(
1562 "Expected Option(U64), got {:?}",
1563 std::mem::discriminant(&other)
1564 ),
1565 }
1566 }
1567
1568 #[test]
1569 fn test_classify_option_vec() {
1570 match classify_field_type(&parse_type("Option<Vec<u8>>")) {
1571 FieldTypeKind::Option(inner) => match *inner {
1572 FieldTypeKind::Vec(elem) => assert!(matches!(*elem, FieldTypeKind::U8)),
1573 _ => panic!("Expected Vec inside Option"),
1574 },
1575 _ => panic!("Expected Option"),
1576 }
1577 }
1578
1579 #[test]
1580 fn test_classify_unknown_defaults_u64() {
1581 assert!(matches!(
1583 classify_field_type(&parse_type("Pubkey")),
1584 FieldTypeKind::U64
1585 ));
1586 assert!(matches!(
1587 classify_field_type(&parse_type("MyCustomType")),
1588 FieldTypeKind::U64
1589 ));
1590 }
1591
1592 #[test]
1597 fn test_byte_size_u64() {
1598 assert_eq!(field_byte_size(&FieldTypeKind::U64, None), 8);
1599 }
1600
1601 #[test]
1602 fn test_byte_size_u128() {
1603 assert_eq!(field_byte_size(&FieldTypeKind::U128, None), 16);
1604 }
1605
1606 #[test]
1607 fn test_byte_size_bool() {
1608 assert_eq!(field_byte_size(&FieldTypeKind::Bool, None), 8);
1609 }
1610
1611 #[test]
1612 fn test_byte_size_vec_default() {
1613 let kind = FieldTypeKind::Vec(Box::new(FieldTypeKind::U64));
1615 assert_eq!(field_byte_size(&kind, None), 8 + 8 * 8);
1616 }
1617
1618 #[test]
1619 fn test_byte_size_vec_max_len() {
1620 let kind = FieldTypeKind::Vec(Box::new(FieldTypeKind::U64));
1622 assert_eq!(field_byte_size(&kind, Some(4)), 8 + 4 * 8);
1623 }
1624
1625 #[test]
1626 fn test_byte_size_option() {
1627 let kind = FieldTypeKind::Option(Box::new(FieldTypeKind::U64));
1629 assert_eq!(field_byte_size(&kind, None), 8);
1630 }
1631
1632 #[test]
1633 fn test_byte_size_option_u128() {
1634 let kind = FieldTypeKind::Option(Box::new(FieldTypeKind::U128));
1636 assert_eq!(field_byte_size(&kind, None), 16);
1637 }
1638
1639 #[test]
1640 fn test_byte_size_vec_u128() {
1641 let kind = FieldTypeKind::Vec(Box::new(FieldTypeKind::U128));
1643 assert_eq!(field_byte_size(&kind, Some(3)), 8 + 3 * 16);
1644 }
1645
1646 #[test]
1647 fn test_byte_size_all_scalars_are_8() {
1648 for kind in [
1649 FieldTypeKind::U8,
1650 FieldTypeKind::U16,
1651 FieldTypeKind::U32,
1652 FieldTypeKind::U64,
1653 FieldTypeKind::I8,
1654 FieldTypeKind::I16,
1655 FieldTypeKind::I32,
1656 FieldTypeKind::I64,
1657 FieldTypeKind::Usize,
1658 FieldTypeKind::Bool,
1659 ] {
1660 assert_eq!(
1661 field_byte_size(&kind, None),
1662 8,
1663 "Scalar {:?} should be 8 bytes",
1664 std::mem::discriminant(&kind)
1665 );
1666 }
1667 }
1668
1669 #[test]
1670 fn test_byte_size_option_vec() {
1671 let kind =
1672 FieldTypeKind::Option(Box::new(FieldTypeKind::Vec(Box::new(FieldTypeKind::U64))));
1673 let actual = field_byte_size(&kind, None);
1674 assert_eq!(
1676 actual, 72,
1677 "Option<Vec<u64>> should match Vec<u64> byte size"
1678 );
1679 }
1680
1681 #[test]
1686 fn test_parse_features_basic() {
1687 let content = "[features]\nfoo = []\nbar = [\"dep\"]\n";
1688 let features = parse_features_from_content(content);
1689 assert_eq!(features, vec!["foo", "bar"]);
1690 }
1691
1692 #[test]
1693 fn test_parse_features_skips_default() {
1694 let content = "[features]\ndefault = [\"foo\"]\nfoo = []\nbar = []\n";
1695 let features = parse_features_from_content(content);
1696 assert_eq!(features, vec!["foo", "bar"]);
1697 }
1698
1699 #[test]
1700 fn test_parse_features_skips_comments() {
1701 let content = "[features]\n# this is a comment\nfoo = []\n# another comment\nbar = []\n";
1702 let features = parse_features_from_content(content);
1703 assert_eq!(features, vec!["foo", "bar"]);
1704 }
1705
1706 #[test]
1707 fn test_parse_features_stops_at_next_section() {
1708 let content = "[features]\nfoo = []\n[dependencies]\nbar = \"1.0\"\n";
1709 let features = parse_features_from_content(content);
1710 assert_eq!(features, vec!["foo"]);
1711 }
1712
1713 #[test]
1714 fn test_parse_features_empty() {
1715 let content = "[dependencies]\nfoo = \"1.0\"\n";
1716 let features = parse_features_from_content(content);
1717 assert!(features.is_empty());
1718 }
1719
1720 #[test]
1721 fn test_parse_features_no_content() {
1722 let features = parse_features_from_content("");
1723 assert!(features.is_empty());
1724 }
1725
1726 #[test]
1731 fn test_extract_vec_inner() {
1732 let ty = parse_type("Vec<u64>");
1733 let inner = extract_vec_inner(&ty);
1734 assert!(inner.is_some());
1735 assert!(matches!(
1736 classify_field_type(inner.unwrap()),
1737 FieldTypeKind::U64
1738 ));
1739 }
1740
1741 #[test]
1742 fn test_extract_option_inner() {
1743 let ty = parse_type("Option<bool>");
1744 let inner = extract_option_inner(&ty);
1745 assert!(inner.is_some());
1746 assert!(matches!(
1747 classify_field_type(inner.unwrap()),
1748 FieldTypeKind::Bool
1749 ));
1750 }
1751
1752 #[test]
1753 fn test_extract_non_generic() {
1754 let ty = parse_type("u64");
1755 assert!(extract_vec_inner(&ty).is_none());
1756 assert!(extract_option_inner(&ty).is_none());
1757 }
1758
1759 #[test]
1760 fn test_extract_wrong_name() {
1761 let ty = parse_type("HashMap<K, V>");
1762 assert!(extract_generic_inner(&ty, "Vec").is_none());
1763 assert!(extract_generic_inner(&ty, "Option").is_none());
1764 }
1765}