Skip to main content

aiken_lang/
test_framework.rs

1use crate::{
2    ast::{
3        BinOp, DataTypeKey, IfBranch, OnTestFailure, Span, TraceLevel, Tracing, TypedArg,
4        TypedDataType, TypedTest,
5    },
6    expr::{CallArg, TypedExpr, UntypedExpr},
7    format::Formatter,
8    gen_uplc::CodeGenerator,
9    plutus_version::PlutusVersion,
10    tipo::{Type, convert_opaque_type},
11};
12use cryptoxide::{blake2b::Blake2b, digest::Digest};
13use indexmap::IndexMap;
14use itertools::Itertools;
15use owo_colors::{OwoColorize, Stream, Stream::Stderr};
16use pallas_primitives::alonzo::{Constr, PlutusData};
17use patricia_tree::PatriciaMap;
18#[cfg(not(target_family = "wasm"))]
19use std::time::Duration;
20use std::{
21    borrow::Borrow,
22    collections::BTreeMap,
23    convert::TryFrom,
24    fmt::{Debug, Display},
25    ops::Deref,
26    path::PathBuf,
27    rc::Rc,
28};
29use uplc::{
30    ast::{Constant, Data, Name, NamedDeBruijn, Program, Term},
31    machine::{cost_model::ExBudget, eval_result::EvalResult},
32};
33use vec1::{Vec1, vec1};
34
35#[derive(Debug, Clone, Copy)]
36pub enum RunnableKind {
37    Test,
38    Bench,
39}
40
41/// ----- Test -----------------------------------------------------------------
42///
43/// Aiken supports two kinds of tests: unit and property. A unit test is a simply
44/// UPLC program which returns must be a lambda that returns a boolean.
45///
46/// A property on the other-hand is a template for generating tests, which is also
47/// a lambda but that takes an extra argument. The argument is generated from a
48/// fuzzer which is meant to yield random values in a pseudo-random (albeit seeded)
49/// sequence. On failures, the value that caused a failure is simplified using an
50/// approach similar to what's described in MiniThesis<https://github.com/DRMacIver/minithesis>,
51/// which is a simplified version of Hypothesis, a property-based testing framework
52/// with integrated shrinking.
53///
54/// Our approach could perhaps be called "microthesis", as it implements a subset of
55/// minithesis. More specifically, we do not currently support pre-conditions, nor
56/// targets.
57///
58#[derive(Debug, Clone)]
59pub enum Test {
60    UnitTest(UnitTest),
61    PropertyTest(PropertyTest),
62    Benchmark(Benchmark),
63}
64
65unsafe impl Send for Test {}
66
67impl Test {
68    pub fn unit_test(
69        generator: &mut CodeGenerator<'_>,
70        test: TypedTest,
71        module_name: String,
72        input_path: PathBuf,
73    ) -> Test {
74        let program = generator.generate_raw(&test.body, &[], &module_name);
75
76        let assertion = match test.body.try_into() {
77            Err(..) => None,
78            Ok(Assertion { bin_op, head, tail }) => {
79                let as_constant = |generator: &mut CodeGenerator<'_>, side| {
80                    Program::<NamedDeBruijn>::try_from(generator.generate_raw(
81                        &side,
82                        &[],
83                        &module_name,
84                    ))
85                    .expect("failed to convert assertion operand to NamedDeBruijn")
86                    .eval(ExBudget::max())
87                    .unwrap_constant()
88                    .map(|cst| (cst, side.tipo()))
89                };
90
91                // Assertion at this point is evaluated so it's not just a normal assertion
92                Some(Assertion {
93                    bin_op,
94                    head: as_constant(generator, head.expect("cannot be Err at this point")),
95                    tail: tail
96                        .expect("cannot be Err at this point")
97                        .try_mapped(|e| as_constant(generator, e)),
98                })
99            }
100        };
101
102        Test::UnitTest(UnitTest {
103            input_path,
104            module: module_name,
105            name: test.name,
106            program,
107            assertion,
108            on_test_failure: test.on_test_failure,
109        })
110    }
111
112    pub fn property_test(
113        input_path: PathBuf,
114        module: String,
115        name: String,
116        on_test_failure: OnTestFailure,
117        program: Program<Name>,
118        fuzzer: Fuzzer<Name>,
119    ) -> Test {
120        Test::PropertyTest(PropertyTest {
121            input_path,
122            module,
123            name,
124            program,
125            on_test_failure,
126            fuzzer,
127        })
128    }
129
130    pub fn from_function_definition(
131        generator: &mut CodeGenerator<'_>,
132        test: TypedTest,
133        module_name: String,
134        input_path: PathBuf,
135        kind: RunnableKind,
136    ) -> Test {
137        if test.arguments.is_empty() {
138            if matches!(kind, RunnableKind::Bench) {
139                unreachable!("benchmark must have at least one argument");
140            } else {
141                Self::unit_test(generator, test, module_name, input_path)
142            }
143        } else {
144            let parameter = test.arguments.first().unwrap().to_owned();
145
146            let via = parameter.via.clone();
147
148            let type_info = parameter.arg.tipo.clone();
149
150            let stripped_type_info = convert_opaque_type(&type_info, generator.data_types(), true);
151
152            let program = generator.clone().generate_raw(
153                &test.body,
154                &[TypedArg {
155                    tipo: stripped_type_info.clone(),
156                    ..parameter.clone().into()
157                }],
158                &module_name,
159            );
160
161            // NOTE: We need not to pass any parameter to the fuzzer/sampler here because the fuzzer
162            // argument is a Data constructor which needs not any conversion. So we can just safely
163            // apply onto it later.
164            let generator_program = generator.clone().generate_raw(&via, &[], &module_name);
165
166            match kind {
167                RunnableKind::Bench => Test::Benchmark(Benchmark {
168                    input_path,
169                    module: module_name,
170                    name: test.name,
171                    program,
172                    on_test_failure: test.on_test_failure,
173                    sampler: Sampler {
174                        program: generator_program,
175                        type_info,
176                        stripped_type_info,
177                    },
178                }),
179                RunnableKind::Test => Self::property_test(
180                    input_path,
181                    module_name,
182                    test.name,
183                    test.on_test_failure,
184                    program,
185                    Fuzzer {
186                        program: generator_program,
187                        stripped_type_info,
188                        type_info,
189                    },
190                ),
191            }
192        }
193    }
194
195    pub fn run(
196        self,
197        seed: u32,
198        max_success: usize,
199        plutus_version: &PlutusVersion,
200        tracing: Tracing,
201    ) -> TestResult<(Constant, Rc<Type>), PlutusData> {
202        match self {
203            Test::UnitTest(unit_test) => {
204                TestResult::UnitTestResult(unit_test.run(plutus_version, tracing))
205            }
206            Test::PropertyTest(property_test) => {
207                TestResult::PropertyTestResult(property_test.run(seed, max_success, plutus_version))
208            }
209            Test::Benchmark(benchmark) => {
210                TestResult::BenchmarkResult(benchmark.run(seed, max_success, plutus_version))
211            }
212        }
213    }
214}
215
216/// ----- UnitTest -----------------------------------------------------------------
217///
218#[derive(Debug, Clone)]
219pub struct UnitTest {
220    pub input_path: PathBuf,
221    pub module: String,
222    pub name: String,
223    pub on_test_failure: OnTestFailure,
224    pub program: Program<Name>,
225    pub assertion: Option<Assertion<(Constant, Rc<Type>)>>,
226}
227
228unsafe impl Send for UnitTest {}
229
230impl UnitTest {
231    pub fn run(
232        self,
233        plutus_version: &PlutusVersion,
234        tracing: Tracing,
235    ) -> UnitTestResult<(Constant, Rc<Type>)> {
236        let eval_result = Program::<NamedDeBruijn>::try_from(self.program.clone())
237            .unwrap()
238            .eval_version(ExBudget::max(), &plutus_version.into());
239
240        let is_evaluation_failure = eval_result.failed(true, &plutus_version.into());
241
242        let success = match self.on_test_failure {
243            OnTestFailure::SucceedEventually | OnTestFailure::SucceedImmediately => {
244                is_evaluation_failure
245            }
246            OnTestFailure::FailImmediately => !is_evaluation_failure,
247        };
248
249        let mut logs = Vec::new();
250        if let Err(err) = eval_result.result()
251            && tracing.trace_level(false) == TraceLevel::Verbose
252        {
253            logs.push(format!("{err}"))
254        }
255        logs.extend(eval_result.logs());
256
257        UnitTestResult {
258            success,
259            test: self.to_owned(),
260            spent_budget: eval_result.cost(),
261            logs,
262            assertion: self.assertion,
263        }
264    }
265}
266
267/// ----- PropertyTest -----------------------------------------------------------------
268
269#[derive(Debug, Clone)]
270pub struct PropertyTest {
271    pub input_path: PathBuf,
272    pub module: String,
273    pub name: String,
274    pub on_test_failure: OnTestFailure,
275    pub program: Program<Name>,
276    pub fuzzer: Fuzzer<Name>,
277}
278
279unsafe impl Send for PropertyTest {}
280
281#[derive(Debug, Clone)]
282pub struct Fuzzer<T> {
283    pub program: Program<T>,
284
285    pub type_info: Rc<Type>,
286
287    /// A version of the Fuzzer's type that has gotten rid of
288    /// all erasable opaque type. This is needed in order to
289    /// generate Plutus data with the appropriate shape.
290    pub stripped_type_info: Rc<Type>,
291}
292
293#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
294#[error("Fuzzer exited unexpectedly: {uplc_error}.")]
295pub struct FuzzerError {
296    logs: Vec<String>,
297    uplc_error: uplc::machine::Error,
298}
299
300#[derive(Debug, Clone)]
301pub enum Event {
302    Simplifying {
303        choices: usize,
304    },
305    Simplified {
306        #[cfg(not(target_family = "wasm"))]
307        duration: Duration,
308        #[cfg(target_family = "wasm")]
309        duration: (),
310        steps: usize,
311    },
312}
313
314impl Display for Event {
315    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
316        match self {
317            Event::Simplifying { choices } => f.write_str(&format!(
318                "{} {}",
319                "  Simplifying"
320                    .if_supports_color(Stderr, |s| s.bold())
321                    .if_supports_color(Stderr, |s| s.purple()),
322                format!("counterexample from {choices} choices")
323                    .if_supports_color(Stderr, |s| s.bold()),
324            )),
325            #[cfg(target_family = "wasm")]
326            Event::Simplified { steps, .. } => f.write_str(&format!(
327                "{} {}",
328                "   Simplified"
329                    .if_supports_color(Stderr, |s| s.bold())
330                    .if_supports_color(Stderr, |s| s.purple()),
331                format!("counterexample after {steps} steps",)
332                    .if_supports_color(Stderr, |s| s.bold()),
333            )),
334            #[cfg(not(target_family = "wasm"))]
335            Event::Simplified { duration, steps } => f.write_str(&format!(
336                "{} {}",
337                "   Simplified"
338                    .if_supports_color(Stderr, |s| s.bold())
339                    .if_supports_color(Stderr, |s| s.purple()),
340                format!(
341                    "counterexample in {} after {steps} steps",
342                    if duration.as_secs() == 0 {
343                        format!("{}ms", duration.as_millis())
344                    } else {
345                        format!("{}s", duration.as_secs())
346                    }
347                )
348                .if_supports_color(Stderr, |s| s.bold()),
349            )),
350        }
351    }
352}
353
354impl PropertyTest {
355    pub const DEFAULT_MAX_SUCCESS: usize = 100;
356
357    /// Run a property test from a given seed. The property is run at most DEFAULT_MAX_SUCCESS times. It
358    /// may stops earlier on failure; in which case a 'counterexample' is returned.
359    pub fn run(
360        self,
361        seed: u32,
362        n: usize,
363        plutus_version: &PlutusVersion,
364    ) -> PropertyTestResult<PlutusData> {
365        let mut labels = BTreeMap::new();
366        let mut remaining = n;
367
368        let (logs, counterexample, iterations) = match self.run_n_times(
369            &mut remaining,
370            Prng::from_seed(seed),
371            &mut labels,
372            plutus_version,
373        ) {
374            Ok(None) => (Vec::new(), Ok(None), n),
375            Ok(Some(counterexample)) => (
376                self.eval(&counterexample.value, plutus_version).logs(),
377                Ok(Some(counterexample.value)),
378                n - remaining,
379            ),
380            Err(FuzzerError { logs, uplc_error }) => (logs, Err(uplc_error), n - remaining + 1),
381        };
382
383        PropertyTestResult {
384            test: self,
385            counterexample,
386            iterations,
387            labels,
388            logs,
389        }
390    }
391
392    pub fn run_n_times<'a>(
393        &'a self,
394        remaining: &mut usize,
395        initial_prng: Prng,
396        labels: &mut BTreeMap<String, usize>,
397        plutus_version: &'a PlutusVersion,
398    ) -> Result<Option<Counterexample<'a>>, FuzzerError> {
399        let mut prng = initial_prng;
400        let mut counterexample = None;
401
402        while *remaining > 0 && counterexample.is_none() {
403            (prng, counterexample) = self.run_once(prng, labels, plutus_version)?;
404            *remaining -= 1;
405        }
406
407        Ok(counterexample)
408    }
409
410    fn run_once<'a>(
411        &'a self,
412        prng: Prng,
413        labels: &mut BTreeMap<String, usize>,
414        plutus_version: &'a PlutusVersion,
415    ) -> Result<(Prng, Option<Counterexample<'a>>), FuzzerError> {
416        use OnTestFailure::*;
417
418        let (next_prng, value) = prng
419            .sample(&self.fuzzer.program)?
420            .expect("A seeded PRNG returned 'None' which indicates a fuzzer is ill-formed and implemented wrongly; please contact library's authors.");
421
422        let result = self.eval(&value, plutus_version);
423
424        for label in result.labels() {
425            // NOTE: There may be other log outputs that interefere with labels. So *by
426            // convention*, we treat as label strings that starts with a NUL byte, which
427            // should be a guard sufficient to prevent inadvertent clashes.
428            labels
429                .entry(label)
430                .and_modify(|count| *count += 1)
431                .or_insert(1);
432        }
433
434        let is_failure = result.failed(true, &plutus_version.into());
435
436        let is_success = !is_failure;
437
438        let keep_counterexample = match self.on_test_failure {
439            FailImmediately | SucceedImmediately => is_failure,
440            SucceedEventually => is_success,
441        };
442
443        if keep_counterexample {
444            let mut counterexample = Counterexample {
445                value,
446                choices: next_prng.choices(),
447                cache: Cache::new(|choices| {
448                    match Prng::from_choices(choices).sample(&self.fuzzer.program) {
449                        Err(..) => Status::Invalid,
450                        Ok(None) => Status::Invalid,
451                        Ok(Some((_, value))) => {
452                            let is_failure = self
453                                .eval(&value, plutus_version)
454                                .failed(true, &plutus_version.into());
455
456                            match self.on_test_failure {
457                                FailImmediately | SucceedImmediately => {
458                                    if is_failure {
459                                        Status::Keep(value)
460                                    } else {
461                                        Status::Ignore
462                                    }
463                                }
464
465                                SucceedEventually => {
466                                    if is_failure {
467                                        Status::Ignore
468                                    } else {
469                                        Status::Keep(value)
470                                    }
471                                }
472                            }
473                        }
474                    }
475                }),
476            };
477
478            if !counterexample.choices.is_empty() {
479                counterexample.simplify();
480            }
481
482            Ok((next_prng, Some(counterexample)))
483        } else {
484            Ok((next_prng, None))
485        }
486    }
487
488    pub fn eval(&self, value: &PlutusData, plutus_version: &PlutusVersion) -> EvalResult {
489        let program = self.program.apply_data(value.clone());
490
491        Program::<NamedDeBruijn>::try_from(program)
492            .unwrap()
493            .eval_version(ExBudget::max(), &plutus_version.into())
494    }
495}
496
497/// ----- Benchmark -----------------------------------------------------------------
498
499#[derive(Debug, Clone)]
500pub struct Sampler<T> {
501    pub program: Program<T>,
502
503    pub type_info: Rc<Type>,
504
505    /// A version of the Fuzzer's type that has gotten rid of
506    /// all erasable opaque type. This is needed in order to
507    /// generate Plutus data with the appropriate shape.
508    pub stripped_type_info: Rc<Type>,
509}
510
511#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
512pub enum BenchmarkError {
513    #[error("Sampler exited unexpectedly: {uplc_error}.")]
514    SamplerError {
515        logs: Vec<String>,
516        uplc_error: uplc::machine::Error,
517    },
518    #[error("Bench exited unexpectedly: {uplc_error}.")]
519    BenchError {
520        logs: Vec<String>,
521        uplc_error: uplc::machine::Error,
522    },
523}
524
525impl BenchmarkError {
526    pub fn logs(&self) -> &[String] {
527        match self {
528            BenchmarkError::SamplerError { logs, .. } | BenchmarkError::BenchError { logs, .. } => {
529                logs.as_slice()
530            }
531        }
532    }
533}
534
535#[derive(Debug, Clone)]
536pub struct Benchmark {
537    pub input_path: PathBuf,
538    pub module: String,
539    pub name: String,
540    pub on_test_failure: OnTestFailure,
541    pub program: Program<Name>,
542    pub sampler: Sampler<Name>,
543}
544
545unsafe impl Send for Benchmark {}
546
547impl Benchmark {
548    pub const DEFAULT_MAX_SIZE: usize = 30;
549
550    pub fn run(
551        self,
552        seed: u32,
553        max_size: usize,
554        plutus_version: &PlutusVersion,
555    ) -> BenchmarkResult {
556        let mut measures = Vec::with_capacity(max_size);
557        let mut prng = Prng::from_seed(seed);
558        let mut error = None;
559        let mut size = 0;
560
561        while error.is_none() && max_size >= size {
562            let fuzzer = self
563                .sampler
564                .program
565                .apply_term(&Term::Constant(Constant::Integer(size.into()).into()));
566
567            match prng.sample(&fuzzer) {
568                Ok(None) => {
569                    panic!(
570                        "A seeded PRNG returned 'None' which indicates a sampler is ill-formed and implemented wrongly; please contact library's authors."
571                    );
572                }
573
574                Ok(Some((new_prng, value))) => {
575                    prng = new_prng;
576                    let result = self.eval(&value, plutus_version);
577                    match result.result() {
578                        Ok(_) => measures.push((size, result.cost())),
579                        Err(uplc_error) => {
580                            error = Some(BenchmarkError::BenchError {
581                                logs: result.logs(),
582                                uplc_error,
583                            });
584                        }
585                    }
586                }
587
588                Err(FuzzerError { logs, uplc_error }) => {
589                    error = Some(BenchmarkError::SamplerError { logs, uplc_error });
590                }
591            }
592
593            size += 1;
594        }
595
596        BenchmarkResult {
597            bench: self,
598            measures,
599            error,
600        }
601    }
602
603    pub fn eval(&self, value: &PlutusData, plutus_version: &PlutusVersion) -> EvalResult {
604        let program = self.program.apply_data(value.clone());
605
606        Program::<NamedDeBruijn>::try_from(program)
607            .unwrap()
608            .eval_version(ExBudget::max(), &plutus_version.into())
609    }
610}
611
612/// ----- PRNG -----------------------------------------------------------------
613///
614/// A Pseudo-random generator (PRNG) used to produce random values for fuzzers.
615/// Note that the randomness isn't actually managed by the Rust framework, it
616/// entirely relies on properties of hashing algorithm on-chain (e.g. blake2b).
617///
618/// The PRNG can have two forms:
619///
620/// 1. Seeded: which occurs during the initial run of a property. Each time a
621///    number is drawn from the PRNG, a new seed is created. We retain all the
622///    choices drawn in a _choices_ vector.
623///
624/// 2. Replayed: which is used to replay a Prng sequenced from a list of known
625///    choices. This happens when shrinking an example. Instead of trying to
626///    shrink the value directly, we shrink the PRNG sequence with the hope that
627///    it will generate a smaller value. This implies that generators tend to
628///    generate smaller values when drawing smaller numbers.
629///
630#[derive(Debug)]
631pub enum Prng {
632    Seeded { choices: Vec<u8>, uplc: PlutusData },
633    Replayed { choices: Vec<u8>, uplc: PlutusData },
634}
635
636impl Prng {
637    /// Constructor tag for Prng's 'Seeded'
638    const SEEDED: u64 = 0;
639    /// Constructor tag for Prng's 'Replayed'
640    const REPLAYED: u64 = 1;
641
642    /// Constructor tag for Option's 'Some'
643    const SOME: u64 = 0;
644    /// Constructor tag for Option's 'None'
645    const NONE: u64 = 1;
646
647    pub fn uplc(&self) -> PlutusData {
648        match self {
649            Prng::Seeded { uplc, .. } => uplc.clone(),
650            Prng::Replayed { uplc, .. } => uplc.clone(),
651        }
652    }
653
654    pub fn choices(&self) -> Vec<u8> {
655        match self {
656            Prng::Seeded { choices, .. } => {
657                let mut choices = choices.to_vec();
658                choices.reverse();
659                choices
660            }
661            Prng::Replayed { choices, .. } => choices.to_vec(),
662        }
663    }
664
665    /// Construct a Pseudo-random number generator from a seed.
666    pub fn from_seed(seed: u32) -> Prng {
667        let mut digest = [0u8; 32];
668        let mut context = Blake2b::new(32);
669        context.input(&seed.to_be_bytes()[..]);
670        context.result(&mut digest);
671
672        Prng::Seeded {
673            choices: vec![],
674            uplc: Data::constr(
675                Prng::SEEDED,
676                vec![
677                    Data::bytestring(digest.to_vec()), // Prng's seed
678                    Data::bytestring(vec![]),          // Random choices
679                ],
680            ),
681        }
682    }
683
684    /// Construct a Pseudo-random number generator from a pre-defined list of choices.
685    pub fn from_choices(choices: &[u8]) -> Prng {
686        Prng::Replayed {
687            uplc: Data::constr(
688                Prng::REPLAYED,
689                vec![
690                    Data::integer(choices.len().into()),
691                    Data::bytestring(choices.iter().rev().cloned().collect::<Vec<_>>()),
692                ],
693            ),
694            choices: choices.to_vec(),
695        }
696    }
697
698    /// Generate a pseudo-random value from a fuzzer using the given PRNG.
699    pub fn sample(
700        &self,
701        fuzzer: &Program<Name>,
702    ) -> Result<Option<(Prng, PlutusData)>, FuzzerError> {
703        let program = Program::<NamedDeBruijn>::try_from(fuzzer.apply_data(self.uplc())).unwrap();
704        let result = program.eval(ExBudget::max());
705        result
706            .result()
707            .map_err(|uplc_error| FuzzerError {
708                logs: result.logs(),
709                uplc_error,
710            })
711            .map(Prng::from_result)
712    }
713
714    /// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following
715    /// signature:
716    ///
717    /// `type Fuzzer<a> = fn(Prng) -> Option<(Prng, a)>`
718    ///
719    /// In nominal scenarios (i.e. when the fuzzer is made from a seed and evolve pseudo-randomly),
720    /// it cannot yield 'None'. When replayed however, we can't easily guarantee that the changes
721    /// made during shrinking aren't breaking underlying invariants (if only, because we run out of
722    /// values to replay). In such case, the replayed sequence is simply invalid and the fuzzer
723    /// aborted altogether with 'None'.
724    pub fn from_result(result: Term<NamedDeBruijn>) -> Option<(Self, PlutusData)> {
725        /// Interpret the given 'PlutusData' as one of two Prng constructors.
726        fn as_prng(cst: &PlutusData) -> Prng {
727            if let PlutusData::Constr(Constr { tag, fields, .. }) = cst {
728                if *tag == 121 + Prng::SEEDED
729                    && let [
730                        PlutusData::BoundedBytes(bytes),
731                        PlutusData::BoundedBytes(choices),
732                    ] = &fields[..]
733                {
734                    return Prng::Seeded {
735                        choices: choices.to_vec(),
736                        uplc: Data::constr(
737                            Prng::SEEDED,
738                            vec![
739                                PlutusData::BoundedBytes(bytes.to_owned()),
740                                // Clear choices between seeded runs, to not
741                                // accumulate ALL choices ever made.
742                                PlutusData::BoundedBytes(vec![].into()),
743                            ],
744                        ),
745                    };
746                }
747
748                if *tag == 121 + Prng::REPLAYED
749                    && let [PlutusData::BigInt(..), PlutusData::BoundedBytes(choices)] = &fields[..]
750                {
751                    return Prng::Replayed {
752                        choices: choices.to_vec(),
753                        uplc: cst.clone(),
754                    };
755                }
756            }
757
758            unreachable!("malformed Prng: {cst:#?}")
759        }
760
761        if let Term::Constant(rc) = &result
762            && let Constant::Data(PlutusData::Constr(Constr { tag, fields, .. })) = &rc.borrow()
763        {
764            if *tag == 121 + Prng::SOME
765                && let [PlutusData::Array(elems)] = &fields[..]
766                && let [new_seed, value] = &elems[..]
767            {
768                return Some((as_prng(new_seed), value.clone()));
769            }
770
771            // May occurs when replaying a fuzzer from a shrinked sequence of
772            // choices. If we run out of choices, or a choice end up being
773            // invalid as per the expectation, the fuzzer can't go further and
774            // fail.
775            if *tag == 121 + Prng::NONE {
776                return None;
777            }
778        }
779
780        unreachable!("Fuzzer yielded a malformed result? {result:#?}")
781    }
782}
783
784/// ----- Counterexample -----------------------------------------------------------------
785///
786/// A counterexample is constructed from a test failure. It holds a value, and a sequence
787/// of random choices that led to this value. It holds a reference to the underlying
788/// property and fuzzer. In many cases, a counterexample can be simplified (a.k.a "shrinked")
789/// into a smaller counterexample.
790pub struct Counterexample<'a> {
791    pub value: PlutusData,
792    pub choices: Vec<u8>,
793    pub cache: Cache<'a, PlutusData>,
794}
795
796impl Counterexample<'_> {
797    fn consider(&mut self, choices: &[u8]) -> bool {
798        if choices == self.choices {
799            return true;
800        }
801
802        match self.cache.get(choices) {
803            Status::Invalid | Status::Ignore => false,
804            Status::Keep(value) => {
805                // If these new choices are shorter or smaller, then we pick them
806                // as new choices and inform that it's been an improvement.
807                if choices.len() <= self.choices.len() || choices < &self.choices[..] {
808                    self.value = value;
809                    self.choices = choices.to_vec();
810                    true
811                } else {
812                    false
813                }
814            }
815        }
816    }
817
818    /// Try to simplify a 'Counterexample' by manipulating the random sequence of generated values
819    /// (a.k.a. choices). While the implementation is quite involved, the strategy is rather simple
820    /// at least conceptually:
821    ///
822    /// Each time a (seeded) fuzzer generates a new value and a new seed, it also stores the
823    /// generated value in a vector, which we call 'choices'. If we re-run the test case with this
824    /// exact choice sequence, we end up with the exact same outcome.
825    ///
826    /// But, we can tweak chunks of this sequence in hope to generate a _smaller sequence_, thus
827    /// generally resulting in a _smaller counterexample_. Each transformations is applied on
828    /// chunks of size 8, 4, 2 and 1; until we no longer make progress (i.e. hit a fix point).
829    ///
830    /// As per MiniThesis, we consider the following transformations:
831    ///
832    /// - Deleting chunks
833    /// - Transforming chunks into sequence of zeroes
834    /// - Replacing chunks of values with smaller values
835    /// - Sorting chunks in ascending order
836    /// - Swapping nearby pairs
837    /// - Redistributing values between nearby pairs
838    pub fn simplify(&mut self) {
839        let mut prev;
840
841        let mut steps = 0;
842
843        #[cfg(not(target_family = "wasm"))]
844        let now = std::time::Instant::now();
845
846        eprintln!(
847            "{}",
848            Event::Simplifying {
849                choices: self.choices.len(),
850            }
851        );
852
853        loop {
854            prev = self.choices.clone();
855
856            // First try deleting each choice we made in chunks. We try longer chunks because this
857            // allows us to delete whole composite elements: e.g. deleting an element from a
858            // generated list requires us to delete both the choice of whether to include it and
859            // also the element itself, which may involve more than one choice.
860            let mut k = 8;
861            while k > 0 {
862                let (mut i, mut underflow) = if self.choices.len() < k {
863                    (0, true)
864                } else {
865                    (self.choices.len() - k, false)
866                };
867
868                while !underflow {
869                    if i >= self.choices.len() {
870                        (i, underflow) = i.overflowing_sub(1);
871                        steps += 1;
872                        continue;
873                    }
874
875                    let j = i + k;
876
877                    let mut choices = [
878                        &self.choices[..i],
879                        if j < self.choices.len() {
880                            &self.choices[j..]
881                        } else {
882                            &[]
883                        },
884                    ]
885                    .concat();
886
887                    if !self.consider(&choices) {
888                        // Perform an extra reduction step that decrease the size of choices near
889                        // the end, to cope with dependencies between choices, e.g. drawing a
890                        // number as a list length, and then drawing that many elements.
891                        //
892                        // This isn't perfect, but allows to make progresses in many cases.
893                        if i > 0 && choices[i - 1] > 0 {
894                            choices[i - 1] -= 1;
895                            if self.consider(&choices) {
896                                i += 1;
897                            };
898                        }
899
900                        (i, underflow) = i.overflowing_sub(1);
901                    }
902
903                    steps += 1;
904                }
905
906                k /= 2
907            }
908
909            if !self.choices.is_empty() {
910                // Now we try replacing region of choices with zeroes. Note that unlike the above we
911                // skip k = 1 because we handle that in the next step. Often (but not always) a block
912                // of all zeroes is the smallest value that a region can be.
913                let mut k = 8;
914                while k > 1 {
915                    let mut i = self.choices.len();
916                    while i >= k {
917                        steps += 1;
918                        let ivs = (i - k..i).map(|j| (j, 0)).collect::<Vec<_>>();
919                        i -= if self.replace(ivs) { k } else { 1 }
920                    }
921                    k /= 2
922                }
923
924                // Replace choices with smaller value, by doing a binary search. This will replace n
925                // with 0 or n - 1, if possible, but will also more efficiently replace it with, a
926                // smaller number than doing multiple subtractions would.
927                let (mut i, mut underflow) = (self.choices.len() - 1, false);
928                while !underflow {
929                    steps += 1;
930                    self.binary_search_replace(0, self.choices[i], |v| vec![(i, v)]);
931                    (i, underflow) = i.overflowing_sub(1);
932                }
933
934                // Sort out of orders chunks in ascending order
935                let mut k = 8;
936                while k > 1 {
937                    let mut i = self.choices.len() - 1;
938                    while i >= k {
939                        steps += 1;
940                        let (from, to) = (i - k, i);
941                        self.replace(
942                            (from..to)
943                                .zip(self.choices[from..to].iter().cloned().sorted())
944                                .collect(),
945                        );
946                        i -= 1;
947                    }
948                    k /= 2
949                }
950
951                // Try adjusting nearby pairs by:
952                //
953                // - Swapping them if they are out-of-order
954                // - Redistributing values between them.
955                for k in [2, 1] {
956                    let mut j = self.choices.len() - 1;
957                    while j >= k {
958                        let i = j - k;
959
960                        // Swap
961                        if self.choices[i] > self.choices[j] {
962                            self.replace(vec![(i, self.choices[j]), (j, self.choices[i])]);
963                        }
964
965                        let iv = self.choices[i];
966                        let jv = self.choices[j];
967
968                        // Replace
969                        if iv > 0 && jv <= u8::MAX - iv {
970                            self.binary_search_replace(0, iv, |v| vec![(i, v), (j, jv + (iv - v))]);
971                        }
972
973                        steps += 1;
974
975                        j -= 1
976                    }
977                }
978            }
979
980            // If we've reached a fixed point, then we cannot shrink further. We've reached a
981            // (local) minimum, which is as good as a counterexample we'll get with this approach.
982            if prev.as_slice() == self.choices.as_slice() {
983                break;
984            }
985        }
986
987        eprintln!(
988            "{}",
989            Event::Simplified {
990                #[cfg(not(target_family = "wasm"))]
991                duration: now.elapsed(),
992                #[cfg(target_family = "wasm")]
993                duration: (),
994                steps,
995            }
996        );
997    }
998
999    /// Try to replace a value with a smaller value by doing a binary search between
1000    /// two extremes. This converges relatively fast in order to shrink down values.
1001    fn binary_search_replace<F>(&mut self, lo: u8, hi: u8, f: F) -> u8
1002    where
1003        F: Fn(u8) -> Vec<(usize, u8)>,
1004    {
1005        if self.replace(f(lo)) {
1006            return lo;
1007        }
1008
1009        let mut lo = lo;
1010        let mut hi = hi;
1011
1012        while lo + 1 < hi {
1013            let mid = lo + (hi - lo) / 2;
1014            if self.replace(f(mid)) {
1015                hi = mid;
1016            } else {
1017                lo = mid;
1018            }
1019        }
1020
1021        hi
1022    }
1023
1024    // Replace values in the choices vector, based on the index-value list provided
1025    // and consider the resulting choices.
1026    fn replace(&mut self, ivs: Vec<(usize, u8)>) -> bool {
1027        let mut choices = self.choices.clone();
1028
1029        for (i, v) in ivs {
1030            if i >= choices.len() {
1031                return false;
1032            }
1033            choices[i] = v;
1034        }
1035
1036        self.consider(&choices)
1037    }
1038}
1039
1040/// ----- Cache -----------------------------------------------------------------------
1041///
1042/// A simple cache as a Patricia-trie to look for already explored options. The simplification
1043/// steps does often generate the same paths and the generation of new test values as well as the
1044/// properties can take a significant time.
1045///
1046/// Yet, sequences have interesting properties:
1047///
1048/// 1. The generation and test execution is entirely deterministic.
1049///
1050///
1051pub struct Cache<'a, T> {
1052    db: PatriciaMap<Status<T>>,
1053    #[allow(clippy::type_complexity)]
1054    run: Box<dyn Fn(&[u8]) -> Status<T> + 'a>,
1055}
1056
1057#[derive(Debug, Clone, Copy, PartialEq)]
1058pub enum Status<T> {
1059    Keep(T),
1060    Ignore,
1061    Invalid,
1062}
1063
1064impl<'a, T> Cache<'a, T>
1065where
1066    T: PartialEq + Clone,
1067{
1068    pub fn new<F>(run: F) -> Cache<'a, T>
1069    where
1070        F: Fn(&[u8]) -> Status<T> + 'a,
1071    {
1072        Cache {
1073            db: PatriciaMap::new(),
1074            run: Box::new(run),
1075        }
1076    }
1077
1078    pub fn size(&self) -> usize {
1079        self.db.len()
1080    }
1081
1082    pub fn get(&mut self, choices: &[u8]) -> Status<T> {
1083        if let Some((prefix, status)) = self.db.get_longest_common_prefix(choices) {
1084            let status = status.clone();
1085            if status != Status::Invalid || prefix == choices {
1086                return status;
1087            }
1088        }
1089
1090        let status = self.run.deref()(choices);
1091
1092        // Clear longer path on non-invalid cases, as we will never reach them
1093        // again due to a now-shorter prefix found.
1094        //
1095        // This hopefully keeps the cache under a reasonable size as we prune
1096        // the tree as we discover shorter paths.
1097        if status != Status::Invalid {
1098            let keys = self
1099                .db
1100                .iter_prefix(choices)
1101                .map(|(k, _)| k)
1102                .collect::<Vec<_>>();
1103            for k in keys {
1104                self.db.remove(k);
1105            }
1106        }
1107
1108        self.db.insert(choices, status.clone());
1109
1110        status
1111    }
1112}
1113
1114// ----------------------------------------------------------------------------
1115//
1116// TestResult
1117//
1118// ----------------------------------------------------------------------------
1119
1120#[derive(Debug, Clone)]
1121pub enum TestResult<U, T> {
1122    UnitTestResult(UnitTestResult<U>),
1123    PropertyTestResult(PropertyTestResult<T>),
1124    BenchmarkResult(BenchmarkResult),
1125}
1126
1127unsafe impl<U, T> Send for TestResult<U, T> {}
1128
1129impl TestResult<(Constant, Rc<Type>), PlutusData> {
1130    pub fn reify(
1131        self,
1132        data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
1133    ) -> TestResult<UntypedExpr, UntypedExpr> {
1134        match self {
1135            TestResult::UnitTestResult(test) => TestResult::UnitTestResult(test.reify(data_types)),
1136            TestResult::PropertyTestResult(test) => {
1137                TestResult::PropertyTestResult(test.reify(data_types))
1138            }
1139            TestResult::BenchmarkResult(result) => TestResult::BenchmarkResult(result),
1140        }
1141    }
1142}
1143
1144impl<U, T> TestResult<U, T> {
1145    pub fn is_success(&self) -> bool {
1146        match self {
1147            TestResult::UnitTestResult(UnitTestResult { success, .. }) => *success,
1148            TestResult::PropertyTestResult(PropertyTestResult {
1149                counterexample: Err(..),
1150                ..
1151            }) => false,
1152            TestResult::PropertyTestResult(PropertyTestResult {
1153                counterexample: Ok(counterexample),
1154                test,
1155                ..
1156            }) => match test.on_test_failure {
1157                OnTestFailure::FailImmediately | OnTestFailure::SucceedEventually => {
1158                    counterexample.is_none()
1159                }
1160                OnTestFailure::SucceedImmediately => counterexample.is_some(),
1161            },
1162            TestResult::BenchmarkResult(BenchmarkResult { error, .. }) => error.is_none(),
1163        }
1164    }
1165
1166    pub fn module(&self) -> &str {
1167        match self {
1168            TestResult::UnitTestResult(UnitTestResult { test, .. }) => test.module.as_str(),
1169            TestResult::PropertyTestResult(PropertyTestResult { test, .. }) => test.module.as_str(),
1170            TestResult::BenchmarkResult(BenchmarkResult { bench, .. }) => bench.module.as_str(),
1171        }
1172    }
1173
1174    pub fn title(&self) -> &str {
1175        match self {
1176            TestResult::UnitTestResult(UnitTestResult { test, .. }) => test.name.as_str(),
1177            TestResult::PropertyTestResult(PropertyTestResult { test, .. }) => test.name.as_str(),
1178            TestResult::BenchmarkResult(BenchmarkResult { bench, .. }) => bench.name.as_str(),
1179        }
1180    }
1181
1182    pub fn logs(&self) -> &[String] {
1183        match self {
1184            TestResult::UnitTestResult(UnitTestResult { logs, .. })
1185            | TestResult::PropertyTestResult(PropertyTestResult { logs, .. }) => logs,
1186            TestResult::BenchmarkResult(BenchmarkResult { error, .. }) => {
1187                error.as_ref().map(|e| e.logs()).unwrap_or_default()
1188            }
1189        }
1190    }
1191}
1192
1193#[derive(Debug, Clone)]
1194pub struct UnitTestResult<T> {
1195    pub success: bool,
1196    pub spent_budget: ExBudget,
1197    pub logs: Vec<String>,
1198    pub test: UnitTest,
1199    pub assertion: Option<Assertion<T>>,
1200}
1201
1202unsafe impl<T> Send for UnitTestResult<T> {}
1203
1204impl UnitTestResult<(Constant, Rc<Type>)> {
1205    pub fn reify(
1206        self,
1207        data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
1208    ) -> UnitTestResult<UntypedExpr> {
1209        UnitTestResult {
1210            success: self.success,
1211            spent_budget: self.spent_budget,
1212            logs: self.logs,
1213            test: self.test,
1214            assertion: self.assertion.and_then(|assertion| {
1215                // No need to spend time/cpu on reifying assertions for successful
1216                // tests since they aren't shown.
1217                if self.success {
1218                    return None;
1219                }
1220
1221                Some(Assertion {
1222                    bin_op: assertion.bin_op,
1223                    head: assertion.head.map(|(cst, tipo)| {
1224                        UntypedExpr::reify_constant(data_types, cst, tipo)
1225                            .expect("failed to reify assertion operand?")
1226                    }),
1227                    tail: assertion.tail.map(|xs| {
1228                        xs.mapped(|(cst, tipo)| {
1229                            UntypedExpr::reify_constant(data_types, cst, tipo)
1230                                .expect("failed to reify assertion operand?")
1231                        })
1232                    }),
1233                })
1234            }),
1235        }
1236    }
1237}
1238
1239#[derive(Debug, Clone)]
1240pub struct PropertyTestResult<T> {
1241    pub test: PropertyTest,
1242    pub counterexample: Result<Option<T>, uplc::machine::Error>,
1243    pub iterations: usize,
1244    pub labels: BTreeMap<String, usize>,
1245    pub logs: Vec<String>,
1246}
1247
1248unsafe impl<T> Send for PropertyTestResult<T> {}
1249
1250impl PropertyTestResult<PlutusData> {
1251    pub fn reify(
1252        self,
1253        data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
1254    ) -> PropertyTestResult<UntypedExpr> {
1255        PropertyTestResult {
1256            counterexample: self.counterexample.map(|ok| {
1257                ok.map(|counterexample| {
1258                    UntypedExpr::reify_data(
1259                        data_types,
1260                        counterexample,
1261                        self.test.fuzzer.type_info.clone(),
1262                    )
1263                    .expect("failed to reify counterexample?")
1264                })
1265            }),
1266            iterations: self.iterations,
1267            test: self.test,
1268            labels: self.labels,
1269            logs: self.logs,
1270        }
1271    }
1272}
1273
1274#[derive(Debug, Clone)]
1275pub struct Assertion<T> {
1276    pub bin_op: BinOp,
1277    pub head: Result<T, ()>,
1278    pub tail: Result<Vec1<T>, ()>,
1279}
1280
1281impl TryFrom<TypedExpr> for Assertion<TypedExpr> {
1282    type Error = ();
1283
1284    fn try_from(body: TypedExpr) -> Result<Self, Self::Error> {
1285        match body {
1286            TypedExpr::BinOp {
1287                name,
1288                tipo,
1289                left,
1290                right,
1291                ..
1292            } if tipo == Type::bool() => {
1293                // 'and' and 'or' are left-associative operators.
1294                match (*right).clone().try_into() {
1295                    Ok(Assertion {
1296                        bin_op,
1297                        head: Ok(head),
1298                        tail: Ok(tail),
1299                        ..
1300                    }) if bin_op == name => {
1301                        let mut both = vec1![head];
1302                        both.extend(tail);
1303                        Ok(Assertion {
1304                            bin_op: name,
1305                            head: Ok(*left),
1306                            tail: Ok(both),
1307                        })
1308                    }
1309                    _ => Ok(Assertion {
1310                        bin_op: name,
1311                        head: Ok(*left),
1312                        tail: Ok(vec1![*right]),
1313                    }),
1314                }
1315            }
1316
1317            // NOTE drill through trace-if-false operators for better errors.
1318            TypedExpr::If {
1319                branches,
1320                final_else,
1321                ..
1322            } => {
1323                if let [
1324                    IfBranch {
1325                        condition, body, ..
1326                    },
1327                ] = &branches[..]
1328                {
1329                    let then_is_true = match body {
1330                        TypedExpr::Var {
1331                            name, constructor, ..
1332                        } => name == "True" && constructor.tipo == Type::bool(),
1333                        _ => false,
1334                    };
1335
1336                    let else_is_wrapped_false = match *final_else {
1337                        TypedExpr::Trace { then, .. } => match *then {
1338                            TypedExpr::Var {
1339                                name, constructor, ..
1340                            } => name == "False" && constructor.tipo == Type::bool(),
1341                            _ => false,
1342                        },
1343                        _ => false,
1344                    };
1345
1346                    if then_is_true && else_is_wrapped_false {
1347                        return condition.to_owned().try_into();
1348                    }
1349                }
1350
1351                Err(())
1352            }
1353
1354            TypedExpr::Trace { then, .. } => (*then).try_into(),
1355
1356            TypedExpr::Sequence { expressions, .. } | TypedExpr::Pipeline { expressions, .. } => {
1357                if let Ok(Assertion {
1358                    bin_op,
1359                    head: Ok(head),
1360                    tail: Ok(tail),
1361                }) = expressions.last().unwrap().to_owned().try_into()
1362                {
1363                    let replace = |expr| {
1364                        let mut expressions = expressions.clone();
1365                        expressions.pop();
1366                        expressions.push(expr);
1367                        TypedExpr::Sequence {
1368                            expressions,
1369                            location: Span::empty(),
1370                        }
1371                    };
1372
1373                    Ok(Assertion {
1374                        bin_op,
1375                        head: Ok(replace(head)),
1376                        tail: Ok(tail.mapped(replace)),
1377                    })
1378                } else {
1379                    Err(())
1380                }
1381            }
1382
1383            TypedExpr::Call {
1384                args,
1385                location,
1386                fun,
1387                ..
1388            } => {
1389                // Unwind backpassing if any, or calls to function that contain binary ops.
1390                if let Some((last_arg, first_args)) = args.split_last()
1391                    && let TypedExpr::Fn {
1392                        body: last_arg_body,
1393                        location: last_arg_location,
1394                        tipo: last_arg_tipo,
1395                        is_capture: last_arg_is_capture,
1396                        return_annotation: last_arg_return_annotation,
1397                        args: last_arg_args,
1398                    } = &last_arg.value
1399                {
1400                    let Assertion { bin_op, head, tail } = Self::try_from(*last_arg_body.clone())?;
1401
1402                    let new_callback_tipo = |body: &TypedExpr| -> Rc<Type> {
1403                        match last_arg_tipo.as_ref() {
1404                            Type::Fn {
1405                                args,
1406                                ret: _ret,
1407                                alias,
1408                            } => Rc::new(Type::Fn {
1409                                args: args.clone(),
1410                                ret: body.tipo(), // Replace the return type to match head.
1411                                alias: alias.clone(),
1412                            }),
1413                            Type::App { .. }
1414                            | Type::Var { .. }
1415                            | Type::Pair { .. }
1416                            | Type::Tuple { .. } => {
1417                                unreachable!(
1418                                    "guard above on 'last_arg.value' guarantees that type is necessarily a function (Fn)"
1419                                )
1420                            }
1421                        }
1422                    };
1423
1424                    let new_fun = |body: &TypedExpr, callback_tipo: Rc<Type>| -> Box<TypedExpr> {
1425                        let fun_tipo = match fun.as_ref().tipo().as_ref() {
1426                            Type::Fn {
1427                                args,
1428                                alias,
1429                                ret: _,
1430                            } => {
1431                                let mut args = args
1432                                    .split_last()
1433                                    .expect("function has at least one arg")
1434                                    .1
1435                                    .to_vec();
1436                                args.push(callback_tipo); // Replace last callback argument
1437                                Rc::new(Type::Fn {
1438                                    args,
1439                                    ret: body.tipo(), // Replace overall return type
1440                                    alias: alias.clone(),
1441                                })
1442                            }
1443                            Type::App { .. }
1444                            | Type::Var { .. }
1445                            | Type::Pair { .. }
1446                            | Type::Tuple { .. } => {
1447                                unreachable!(
1448                                    "guard above on 'last_arg.value' guarantees that type is necessarily a function (Fn)"
1449                                )
1450                            }
1451                        };
1452
1453                        let mut fun = fun.clone();
1454                        fun.replace_type(fun_tipo);
1455
1456                        fun
1457                    };
1458
1459                    let new_args =
1460                        |body: TypedExpr, callback_tipo: Rc<Type>| -> Vec<CallArg<TypedExpr>> {
1461                            let mut args = first_args.to_vec();
1462                            args.push(CallArg {
1463                                label: last_arg.label.clone(),
1464                                location: last_arg.location,
1465                                value: TypedExpr::Fn {
1466                                    location: *last_arg_location,
1467                                    tipo: callback_tipo.clone(),
1468                                    is_capture: *last_arg_is_capture,
1469                                    return_annotation: last_arg_return_annotation.clone(),
1470                                    args: last_arg_args.clone(),
1471                                    body: Box::new(body),
1472                                },
1473                            });
1474                            args
1475                        };
1476
1477                    return Ok(Assertion {
1478                        bin_op,
1479                        head: head.map(|body| {
1480                            let callback_tipo = new_callback_tipo(&body);
1481                            TypedExpr::Call {
1482                                location,
1483                                tipo: body.tipo(),
1484                                fun: new_fun(&body, callback_tipo.clone()),
1485                                args: new_args(body, callback_tipo.clone()),
1486                            }
1487                        }),
1488                        tail: tail.map(|tail| {
1489                            tail.mapped(|body| {
1490                                let callback_tipo = new_callback_tipo(&body);
1491                                TypedExpr::Call {
1492                                    location,
1493                                    tipo: body.tipo(),
1494                                    fun: new_fun(&body, callback_tipo.clone()),
1495                                    args: new_args(body, callback_tipo.clone()),
1496                                }
1497                            })
1498                        }),
1499                    });
1500                }
1501                Err(())
1502            }
1503
1504            _ => Err(()),
1505        }
1506    }
1507}
1508
1509pub struct AssertionStyleOptions<'a> {
1510    red: Box<dyn Fn(String) -> String + 'a>,
1511    bold: Box<dyn Fn(String) -> String + 'a>,
1512}
1513
1514impl<'a> AssertionStyleOptions<'a> {
1515    pub fn new(stream: Option<&'a Stream>) -> Self {
1516        match stream {
1517            Some(stream) => Self {
1518                red: Box::new(|s| {
1519                    s.if_supports_color(stream.to_owned(), |s| s.red())
1520                        .to_string()
1521                }),
1522                bold: Box::new(|s| {
1523                    s.if_supports_color(stream.to_owned(), |s| s.bold())
1524                        .to_string()
1525                }),
1526            },
1527            None => Self {
1528                red: Box::new(|s| s),
1529                bold: Box::new(|s| s),
1530            },
1531        }
1532    }
1533}
1534
1535impl Assertion<UntypedExpr> {
1536    #[allow(clippy::just_underscores_and_digits)]
1537    pub fn to_string(&self, expect_failure: bool, style: &AssertionStyleOptions) -> String {
1538        let red = |s: &str| style.red.as_ref()(s.to_string());
1539        let x = |s: &str| style.red.as_ref()(style.bold.as_ref()(format!("× {s}")));
1540
1541        // head did not map to a constant
1542        if self.head.is_err() {
1543            return x("program failed");
1544        }
1545
1546        // any value in tail did not map to a constant
1547        if self.tail.is_err() {
1548            return x("program failed");
1549        }
1550
1551        fn fmt_side(side: &UntypedExpr, red: &dyn Fn(&str) -> String) -> String {
1552            let __ = red("│");
1553
1554            Formatter::new()
1555                .expr(side, false)
1556                .to_pretty_string(60)
1557                .lines()
1558                .map(|line| format!("{__} {line}"))
1559                .collect::<Vec<String>>()
1560                .join("\n")
1561        }
1562
1563        let left = fmt_side(self.head.as_ref().unwrap(), &red);
1564
1565        let tail = self.tail.as_ref().unwrap();
1566
1567        let right = fmt_side(tail.first(), &red);
1568
1569        format!(
1570            "{}{}{}",
1571            x("expected"),
1572            if expect_failure && self.bin_op == BinOp::Or {
1573                x(" neither\n")
1574            } else {
1575                "\n".to_string()
1576            },
1577            if expect_failure {
1578                match self.bin_op {
1579                    BinOp::And => [
1580                        left,
1581                        x("and"),
1582                        [
1583                            tail.mapped_ref(|s| fmt_side(s, &red))
1584                                .join(format!("\n{}\n", x("and")).as_str()),
1585                            if tail.len() > 1 {
1586                                x("to not all be true")
1587                            } else {
1588                                x("to not both be true")
1589                            },
1590                        ]
1591                        .join("\n"),
1592                    ],
1593                    BinOp::Or => [
1594                        left,
1595                        x("nor"),
1596                        [
1597                            tail.mapped_ref(|s| fmt_side(s, &red))
1598                                .join(format!("\n{}\n", x("nor")).as_str()),
1599                            x("to be true"),
1600                        ]
1601                        .join("\n"),
1602                    ],
1603                    BinOp::Eq => [left, x("to not equal"), right],
1604                    BinOp::NotEq => [left, x("to not be different"), right],
1605                    BinOp::LtInt => [left, x("to not be lower than"), right],
1606                    BinOp::LtEqInt => [left, x("to not be lower than or equal to"), right],
1607                    BinOp::GtInt => [left, x("to not be greater than"), right],
1608                    BinOp::GtEqInt => [left, x("to not be greater than or equal to"), right],
1609                    _ => unreachable!("unexpected non-boolean binary operator in assertion?"),
1610                }
1611                .join("\n")
1612            } else {
1613                match self.bin_op {
1614                    BinOp::And => [
1615                        left,
1616                        x("and"),
1617                        [
1618                            tail.mapped_ref(|s| fmt_side(s, &red))
1619                                .join(format!("\n{}\n", x("and")).as_str()),
1620                            if tail.len() > 1 {
1621                                x("to all be true")
1622                            } else {
1623                                x("to both be true")
1624                            },
1625                        ]
1626                        .join("\n"),
1627                    ],
1628                    BinOp::Or => [
1629                        left,
1630                        x("or"),
1631                        [
1632                            tail.mapped_ref(|s| fmt_side(s, &red))
1633                                .join(format!("\n{}\n", x("or")).as_str()),
1634                            x("to be true"),
1635                        ]
1636                        .join("\n"),
1637                    ],
1638                    BinOp::Eq => [left, x("to equal"), right],
1639                    BinOp::NotEq => [left, x("to not equal"), right],
1640                    BinOp::LtInt => [left, x("to be lower than"), right],
1641                    BinOp::LtEqInt => [left, x("to be lower than or equal to"), right],
1642                    BinOp::GtInt => [left, x("to be greater than"), right],
1643                    BinOp::GtEqInt => [left, x("to be greater than or equal to"), right],
1644                    _ => unreachable!("unexpected non-boolean binary operator in assertion?"),
1645                }
1646                .join("\n")
1647            }
1648        )
1649    }
1650}
1651
1652#[derive(Debug, Clone)]
1653pub struct BenchmarkResult {
1654    pub bench: Benchmark,
1655    pub measures: Vec<(usize, ExBudget)>,
1656    pub error: Option<BenchmarkError>,
1657}
1658
1659unsafe impl Send for BenchmarkResult {}
1660unsafe impl Sync for BenchmarkResult {}
1661
1662#[cfg(test)]
1663mod test {
1664    use super::*;
1665
1666    #[test]
1667    fn test_cache() {
1668        let called = std::cell::RefCell::new(0);
1669
1670        let mut cache = Cache::new(|choices| {
1671            called.replace_with(|n| *n + 1);
1672
1673            match choices {
1674                [0, 0, 0] => Status::Keep(true),
1675                _ => {
1676                    if choices.len() <= 2 {
1677                        Status::Invalid
1678                    } else {
1679                        Status::Ignore
1680                    }
1681                }
1682            }
1683        });
1684
1685        assert_eq!(cache.get(&[1, 1]), Status::Invalid); // Fn executed
1686        assert_eq!(cache.get(&[1, 1, 2, 3]), Status::Ignore); // Fn executed
1687        assert_eq!(cache.get(&[1, 1, 2]), Status::Ignore); // Fnexecuted
1688        assert_eq!(cache.get(&[1, 1, 2, 2]), Status::Ignore); // Cached result
1689        assert_eq!(cache.get(&[1, 1, 2, 1]), Status::Ignore); // Cached result
1690        assert_eq!(cache.get(&[0, 1, 2]), Status::Ignore); // Fn executed
1691        assert_eq!(cache.get(&[0, 0, 0]), Status::Keep(true)); // Fn executed
1692        assert_eq!(cache.get(&[0, 0, 0]), Status::Keep(true)); // Cached result
1693
1694        assert_eq!(called.borrow().deref().to_owned(), 5, "execution calls");
1695        assert_eq!(cache.size(), 4, "cache size");
1696    }
1697}