//! Benchmarks for converting from/to (decimal) strings, the only operations
//! that (may) need to allocate, and also some of the few that aren't `O(1)`
//! (alongside e.g. div/mod, but even those likely have a better bound).

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use std::fmt::{self, Write as _};

struct Sample {
    name: &'static str,
    decimal_str: &'static str,
}

impl fmt::Display for Sample {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // HACK(eddyb) this is mostly to sort criterion's output correctly.
        write!(f, "[len={:02}] ", self.decimal_str.len())?;
        f.write_str(self.decimal_str)?;
        if !self.name.is_empty() {
            write!(f, " aka {}", self.name)?;
        }
        Ok(())
    }
}

impl Sample {
    const fn new(decimal_str: &'static str) -> Self {
        Self { name: "", decimal_str }
    }

    const fn named(self, name: &'static str) -> Self {
        Self { name, ..self }
    }
}

const DOUBLE_SAMPLES: &[Sample] = &[
    Sample::new("0.0"),
    Sample::new("1.0"),
    Sample::new("1234.56789"),
    Sample::new("3.14159265358979323846264338327950288").named("Ï€"),
    Sample::new("0.693147180559945309417232121458176568").named("ln(2)"),
];

fn double_from_str(c: &mut Criterion) {
    let mut group = c.benchmark_group("Double::from_str");
    for sample in DOUBLE_SAMPLES {
        group.bench_with_input(BenchmarkId::from_parameter(sample), sample.decimal_str, |b, s| {
            b.iter(|| s.parse::<rustc_apfloat::ieee::Double>().unwrap());
        });
    }
    group.finish();
}

/// `fmt::Write` implementation that does not need to allocate at all,
/// but instead asserts that what's written matches a known string exactly.
struct CheckerFmtSink<'a> {
    remaining: &'a str,
}

impl fmt::Write for CheckerFmtSink<'_> {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        self.remaining = self.remaining.strip_prefix(s).ok_or(fmt::Error)?;
        Ok(())
    }
}

impl CheckerFmtSink<'_> {
    fn finish(self) -> fmt::Result {
        if self.remaining.is_empty() {
            Ok(())
        } else {
            Err(fmt::Error)
        }
    }
}

fn double_to_str(c: &mut Criterion) {
    let mut group = c.benchmark_group("Double::to_str");
    for sample in DOUBLE_SAMPLES {
        let value = sample.decimal_str.parse::<rustc_apfloat::ieee::Double>().unwrap();

        // `CheckerFmtSink` is used later to ensure the formatting doesn't get
        // optimized away, but without allocating - we can, however, allocate
        // the expected output here, ahead of time, and also sanity-check it
        // in a more convenient (and user-friendly) way, ensuring that benching
        // itself never panics (though not in a way the optimizer would know of).
        let value_to_string = &value.to_string();

        // NOTE(eddyb) we only check that we get back the same floating-point
        // `value`, without comparing `value_to_string` and `sample.decimal_str`,
        // because `rustc_apfloat` (correctly) considers "natural precision" can
        // be shorter than our samples, and also it always strips trailing `.0`
        // (outside of scientific notation) - while it is possible to approximate
        // "is this plausibly close enough", it's an irrelevant complication here.
        assert_eq!(value_to_string.parse::<rustc_apfloat::ieee::Double>().unwrap(), value);

        group.bench_with_input(
            BenchmarkId::from_parameter(sample),
            &(value, value_to_string),
            |b, &(value, sample_to_string)| {
                b.iter(|| {
                    let mut checker = CheckerFmtSink {
                        remaining: sample_to_string,
                    };
                    write!(checker, "{value}").unwrap();
                    checker.finish().unwrap();
                });
            },
        );
    }
    group.finish();
}

criterion_group!(benches, double_from_str, double_to_str);
criterion_main!(benches);