/*
 * SPDX-License-Identifier: BSD-3-Clause
 *
 * Copyright © 2020 Keith Packard
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials provided
 *    with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#define _GNU_SOURCE
#include <fenv.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#ifdef __STDC_IEC_559__
#define HAVE_HW_DOUBLE
#endif

#ifdef HAVE_HW_DOUBLE
typedef double test_t;
#define test_sqrt(x) sqrt(x)
#define test_pow(x,y) pow(x,y)
#define test_remainder(x,y) remainder(x,y)
#define test_log(x) log(x)
#define tiny_val 1e-300
#define huge_val 1e300;
#else
typedef float test_t;
#define test_sqrt(x) sqrtf(x)
#define test_pow(x,y) powf(x,y)
#define test_remainder(x,y) remainderf(x,y)
#define test_log(x) logf(x)
#define tiny_val 1e-30f
#define huge_val 1e30f
#endif

volatile test_t one = 1.0;
volatile test_t zero = 0.0;
volatile test_t two = 2.0;
volatile test_t huge = huge_val;
volatile test_t tiny = tiny_val;
volatile test_t inf = (test_t) INFINITY;

#define lowbit(x) 	((x) & -(x))
#define ispoweroftwo(x)	(((x) & ((x) - 1)) == 0)

#ifdef FE_DIVBYZERO
#define my_divbyzero FE_DIVBYZERO
#else
#define my_divbyzero 0
#endif

#ifdef FE_OVERFLOW
#define my_overflow FE_OVERFLOW
#else
#define my_overflow 0
#endif

#ifdef FE_UNDERFLOW
#define my_underflow FE_UNDERFLOW
#else
#define my_underflow 0
#endif

#ifdef FE_INEXACT
#define my_inexact FE_INEXACT
#else
#define my_inexact 0
#endif

#ifdef FE_INVALID
#define my_invalid FE_INVALID
#else
#define my_invalid 0
#endif

static const char *
e_to_str(int e)
{
	if (e == 0)
		return "NONE";

#ifdef FE_DIVBYZERO
	if (e == FE_DIVBYZERO)
		return "FE_DIVBYZERO";
#endif
#ifdef FE_OVERFLOW
	if (e == FE_OVERFLOW)
		return "FE_OVERFLOW";
#endif
#ifdef FE_UNDERFLOW
	if (e == FE_UNDERFLOW)
		return "FE_UNDERFLOW";
#endif
#ifdef FE_INEXACT
	if (e == FE_INEXACT)
		return "FE_INEXACT";
#endif
#ifdef FE_INVALID
	if (e == FE_INVALID)
		return "FE_INVALID";
#endif
#if defined(FE_OVERFLOW) && defined(FE_INEXACT)
	if (e == (FE_OVERFLOW|FE_INEXACT))
		return "FE_OVERFLOW|FE_INEXACT";
#endif
#if defined(FE_UNDERFLOW) && defined(FE_INEXACT)
	if (e == (FE_UNDERFLOW|FE_INEXACT))
		return "FE_UNDERFLOW|FE_INEXACT";
#endif
	static char buf[50];
	sprintf(buf, "Invalid 0x%x", e);
	return buf;
}

#define s(e) #e

static int
report(char *expr, test_t v, int e, int exception, int oexception)
{
        /* powerpc has additional details in the exception flags */
        e &= (my_inexact | my_divbyzero | my_underflow | my_overflow | my_invalid);
	printf("%-20.20s: ", expr);
	printf("%8g ", (double) v);
	printf("expect %s", e_to_str(exception));
	if (oexception)
		printf(" or %s", e_to_str(oexception));
	printf(" got %s\n", e_to_str(e));
	if (e == (exception) ||
	    (oexception && e == (oexception)))
	{
		return 0;
	}
	printf("\tFAILURE\n");
	return 1;
}

#define TEST_CASE2(expr, exception, oexception) do {			\
		int e;							\
		volatile test_t v;					\
		feclearexcept(FE_ALL_EXCEPT);				\
		v = expr;						\
		e = fetestexcept(FE_ALL_EXCEPT);			\
		result += report(s(expr), v, e, exception, oexception); \
	} while(0)

#define TEST_CASE(expr, exception) do {					\
		if ((exception & (my_overflow|my_underflow)) && my_inexact != 0) \
			TEST_CASE2(expr, exception, exception | my_inexact); \
		else							\
			TEST_CASE2(expr, exception, 0);			\
	} while(0)

static const struct {
    const char *name;
    int value;
} excepts[] = {
    { .name = "None", .value = 0 },
#if FE_DIVBYZERO
    { .name = "Divide by zero", .value = FE_DIVBYZERO },
#endif
#if FE_OVERFLOW
    { .name = "Overflow", .value = FE_OVERFLOW },
#endif
#if FE_UNDERFLOW
    { .name = "Underflow", .value = FE_UNDERFLOW },
#endif
#if FE_INVALID
    { .name = "Invalid", .value = FE_INVALID },
#endif
};

#define NUM_EXCEPTS (sizeof(excepts)/sizeof(excepts[0]))

int main(void)
{
	int result = 0;
        int ret;
        unsigned i;

	(void) report;
	(void) e_to_str;
	if (math_errhandling & MATH_ERREXCEPT) {
#if FE_DIVBYZERO
		TEST_CASE(one / zero, FE_DIVBYZERO);
		TEST_CASE(test_log(zero), FE_DIVBYZERO);
#endif
#if FE_OVERFLOW
		TEST_CASE(huge * huge, FE_OVERFLOW);
		TEST_CASE(test_pow(two, huge), FE_OVERFLOW);
#endif
#if FE_UNDERFLOW
		TEST_CASE(tiny * tiny, FE_UNDERFLOW);
		TEST_CASE(test_pow(two, -huge), FE_UNDERFLOW);
#endif
#if FE_INVALID
		TEST_CASE(zero * inf, FE_INVALID);
		TEST_CASE(inf * zero, FE_INVALID);
		TEST_CASE(inf + -inf, FE_INVALID);
		TEST_CASE(inf - inf, FE_INVALID);
		TEST_CASE(zero / zero, FE_INVALID);
		TEST_CASE(inf / inf, FE_INVALID);
		TEST_CASE(test_remainder(one, zero), FE_INVALID);
		TEST_CASE(test_remainder(inf, two), FE_INVALID);
		TEST_CASE(test_sqrt(-two), FE_INVALID);
#endif
	}

	feclearexcept(FE_ALL_EXCEPT);
        for (i = 0; i < NUM_EXCEPTS; i++) {
            ret = feenableexcept(excepts[i].value);
            if (ret == 0) {
                ret = fedisableexcept(excepts[i].value);
                if (ret != excepts[i].value) {
                    printf("enable %s worked, disabled returned %d\n", excepts[i].name, ret);
                    result = 1;
                }
            } else {
                if (excepts[i].value == 0) {
                    printf("enable %s returned %d", excepts[i].name, ret);
                    result = 1;
                }
            }
        }
	return result;
}
