Skip to main content

crucible_invariant_macro/
lib.rs

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
43// Field type classification for FuzzAction code generation
44enum 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, // all scalar types serialize as u64
120            };
121            8 + ml * elem_size // 8-byte length prefix + elements
122        }
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 // Option<Vec<T>> uses same layout as Vec<T>
132            }
133            FieldTypeKind::U128 | FieldTypeKind::I128 => 16,
134            _ => 8, // Option<T> serializes as u64 (value or u64::MAX for None)
135        },
136        _ => 8, // all scalar types (bool, u8, u16, u32, u64, i*, usize) serialize as u64
137    }
138}
139
140// ── helpers that produce *expressions* (no trailing comma / semicolon) ──
141
142/// Expression that produces a random value of the given inner kind.
143fn 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        // u64 — boundary-aware generation
150        (FieldTypeKind::U64, Some(c)) => {
151            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // u128 — boundary-aware generation
156        (FieldTypeKind::U128, Some(c)) => {
157            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // u8/u16/u32 — boundary-aware via gen_u64 then cast
162        (FieldTypeKind::U8 | FieldTypeKind::U16 | FieldTypeKind::U32, Some(c)) => {
163            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // i64 — boundary-aware generation
172        (FieldTypeKind::I64, Some(c)) => {
173            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // i128 — boundary-aware generation
178        (FieldTypeKind::I128, Some(c)) => {
179            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // i8/i16/i32 — boundary-aware via gen_i64 then cast
186        (FieldTypeKind::I8 | FieldTypeKind::I16 | FieldTypeKind::I32, Some(c)) => {
187            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // usize — boundary-aware generation
196        (FieldTypeKind::Usize, Some(c)) => {
197            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // bool — no boundary concept
202        (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
209/// Statement that mutates a value through a mutable reference `ref_tok`.
210/// `inner_ty` is the concrete type (e.g. u8, i32) for small-type widen/narrow.
211fn 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        // u64 — direct call
219        (FieldTypeKind::U64, Some(c)) => {
220            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // u128 — direct call
227        (FieldTypeKind::U128, Some(c)) => {
228            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // u8/u16/u32 — widen to u64, mutate, narrow back
235        (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(&quote! { 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        // i64 — direct call
250        (FieldTypeKind::I64, Some(c)) => {
251            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // i128 — direct call
258        (FieldTypeKind::I128, Some(c)) => {
259            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // i8/i16/i32 — widen to i64, mutate, narrow back
266        (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(&quote! { 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        // usize — dedicated function
281        (FieldTypeKind::Usize, Some(c)) => {
282            let (lo, hi) = c.exclusive_bounds(&quote! { 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        // bool
289        (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
298// ── top-level code-gen functions ──
299
300/// Generate code for a random field value in FuzzAction::random_variant
301fn 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
339/// Generate code for a mutation match arm in FuzzAction::mutate
340fn 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
404/// Generate code for serializing a field in FuzzAction::serialize_fields
405fn 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    // Expression that converts a value to le bytes.
458    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
494/// Generate code for deserializing a field in FuzzAction::deserialize_fields
495fn 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
640/// Generate code for extracting a field from a JSON params object.
641/// Used by `from_name_and_params` to reconstruct actions from .meta.json.
642fn 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
720/// Generate constraint expression, handling Option<T> and Vec<T> by constraining inner values.
721fn 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    // Find all action_* methods and their range constraints
752    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    // Expand include!() macros to discover actions defined in external files.
758    // Also replace include!() items in the impl block with their expanded content,
759    // because the compiler won't re-expand include!() in proc macro output.
760    // Replace include!() macro items with expanded methods in the impl block
761    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    // Helper closure to process a method and extract action info
786    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    // (include!() methods are already expanded into input.items above)
854
855    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    // Get Enum Variants, one for each action
863    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    // Generate arms to get action name as string
875    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    // Generate arms to convert action to JSON value (for .meta.json)
889    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    // Generate constrain_in_place method to apply constraints to the inputs
925    let constrain_arms: Vec<_> = actions
926        .iter()
927        .map(|(action_name, _, params)| {
928            // Get each constraint for the field
929            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    // ===== Generate FuzzAction trait implementation arms =====
954    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            // Compute cumulative byte offsets for variable-width fields (Vec)
982            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            // Generate from_name_and_params arm for this variant
1076            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                /// Get the action name as a string (for coverage tracking)
1110                pub fn action_name(&self) -> &'static str {
1111                    match self {
1112                        #(#action_name_arms)*
1113                    }
1114                }
1115
1116                /// Get the action parameters as a JSON value (for .meta.json)
1117                pub fn to_json_params(&self) -> crucible_test_context::serde_json::Value {
1118                    match self {
1119                        #(#to_json_arms)*
1120                    }
1121                }
1122            }
1123
1124            // FuzzAction trait implementation for structured mutation support
1125            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                /// Dispatch an action and return whether it succeeded.
1186                /// Works with actions that return () (always success) or Result<(), E> (success/failure).
1187                pub fn __dispatch_action(&mut self, action: #enum_name) -> bool {
1188                    use crucible_test_context::IntoActionSuccess;
1189
1190                    // Set current instruction name for coverage tracking
1191                    crucible_test_context::set_current_instruction(Some(action.action_name().to_string()));
1192
1193                    // Dispatch the action and convert result to success bool
1194                    let success = match action {
1195                        #(#dispatch_arms)*
1196                    };
1197
1198                    // Clear after action
1199                    crucible_test_context::set_current_instruction(None);
1200
1201                    // Call after_action callback if defined
1202                    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    // Add after_action callback if the method exists
1216    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                    // No after_action callback defined
1233                }
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    // Structured mutation is always used for invariant_test — no arguments accepted
1273    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    // Extract fixture type from first parameter
1288    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    // Extract type from &mut FixtureType or &FixtureType
1301    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            // Clear action history and violation tracking at start of iteration
1348            crucible_test_context::clear_iteration_state();
1349            // Track total actions for early exit display
1350            crucible_test_context::set_total_actions(capped_len);
1351            // Set test name for metadata
1352            crucible_test_context::set_current_test_name(#test_name_str);
1353            // Set total variant count for monitor display (idempotent)
1354            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            // Keep executed actions for backfilling JSON params on crash/violation.
1360            // Only the action names are recorded during the hot loop (push_action_record_lite),
1361            // and full JSON params are materialized lazily when needed.
1362            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                // Execute the action and get success status
1374                let success = fixture.__dispatch_action(action.clone());
1375
1376                if success {
1377                    crucible_test_context::mark_variant_succeeded(variant_idx);
1378                }
1379
1380                // Record lite action (no JSON serialization — deferred to crash/violation)
1381                crucible_test_context::push_action_record_lite(action.action_name(), success);
1382
1383                // Keep the action for potential param backfill (move, no extra clone)
1384                __executed_actions.push(action);
1385
1386                #fn_body
1387
1388                // Per-action replay diagnostic: log success + violation + state hash
1389                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                // === EARLY EXIT: Stop immediately if invariant was violated ===
1402                if crucible_test_context::has_violation() {
1403                    // Backfill JSON params for ALL executed actions (needed for crash metadata + fuzz show)
1404                    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                // Stop chain on first failure in stateful mode
1412                // (failed actions produce dead-end states, continuing wastes SVM executions)
1413                if !success && crucible_test_context::is_stateful_chain_mode() {
1414                    break;
1415                }
1416            }
1417
1418            // Backfill ALL action params after the loop if not already done by violation.
1419            // This ensures `fuzz show --replay` and crash metadata always have full params.
1420            // Only runs in replay/verbose mode — not on the normal fuzzing hot path.
1421            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
1459/// Parse `[features]` section from Cargo.toml content string.
1460/// Extracted for testability (read_cargo_features reads from disk).
1461fn 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    // ========================================================================
1495    // classify_field_type tests
1496    // ========================================================================
1497
1498    #[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        // Unknown type names default to U64
1582        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    // ========================================================================
1593    // field_byte_size tests
1594    // ========================================================================
1595
1596    #[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        // Vec(U64) with no max_len → 8 + 8*8 = 72
1614        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        // Vec(U64) with max_len=4 → 8 + 4*8 = 40
1621        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        // Option(U64) → 8
1628        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        // Option(U128) → 16
1635        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        // Vec(U128) with max_len=3 → 8 + 3*16 = 56
1642        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        // Option<Vec<u64>> should match Vec<u64> byte size: 8-byte length prefix + 8*8 elements
1675        assert_eq!(
1676            actual, 72,
1677            "Option<Vec<u64>> should match Vec<u64> byte size"
1678        );
1679    }
1680
1681    // ========================================================================
1682    // parse_features_from_content tests
1683    // ========================================================================
1684
1685    #[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    // ========================================================================
1727    // extract_generic_inner / type helper tests
1728    // ========================================================================
1729
1730    #[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}