Skip to content

Instantly share code, notes, and snippets.

@Marc-B-Reynolds
Last active July 8, 2026 16:58
Show Gist options
  • Select an option

  • Save Marc-B-Reynolds/1e2ef2a7e59d100e39d26cf69079ded6 to your computer and use it in GitHub Desktop.

Select an option

Save Marc-B-Reynolds/1e2ef2a7e59d100e39d26cf69079ded6 to your computer and use it in GitHub Desktop.
first pass "floating point option" shield
// -*- coding: utf-8 -*-
//
// NOTE: move to this repo: https://github.com/Marc-B-Reynolds/fp_compiler_shield
//
// Marc B. Reynolds, 2026
// Public Domain under http://unlicense.org, see link for details.
//
// This header include compile-time and runtime checks for compiler
// options that "break" floating-point portability. This optionally
// includes options that are "correct" but hurt performance supporting
// features that the vast majority of user shouldn't want.
//
// USAGE: include this file in any source file (and/or any header
// which contains inline functions which need protecting) and:
// ∙ any "bad" compiler options that can be detected by a predefined
// macro will trigger a static_assert with a message. These
// can be disabled by defining FP_CC_COMPILE_TIME_DISABLED
// before including this header.
// ∙ at initialization time (before 'main' is executed) each
// translation unit which includes this header run a series
// of runtime checks.
//
// LIMITATIONS:
// ∙ There's no automagic way (I can think of) to expand the name
// of the source file so need to define FP_CC_FILE for output
// to not report "(unknown)" (__FILE__ expands to this header)
// ∙ runtime checks (obviously) expand by the compiler settings
// at the point this header is included. So can't catch if they
// are modified in the source file (pragmas, etc) at some later
// point in the file. Main "concern" here is attempting to
// protect some "inline" function (in header to "user").
// ∙ lowest optimization levels might trigger errors and warnings.
// GCC: as of 14.2.0 with -O0 -fno-math-errno the no math errno
// gets ignored and we would get a warning if it wasn't short
// circuited below.
// ∙ Currently with GCC the init time functions are called before
// the one in `fastmathcrt` so it will not detect flushing
// denormals to zero since the control word has not been yet
// changed.
// ∙ In MSVC the runtime checks will not happen if global optimizations
// have been enabled (/GL). The compiler thinks they are
// unreachable and are removed. (can't think of a zero user friction
// way to correct this).
// ∙ many more!
//
// NOTES:
// ∙ assumes at least C99
// ∙ there are purposefully redundant compile & runtime checks
// (lack of faith in compilers, evolution of compilers and myself)
//
//────────────────────────────────────────────────────────────────────────
// SUPPORTED CHECKS and configuration
// ∙ compile time checks can be disable by: #define FP_CC_COMPILE_TIME_DISABLED
// Mainly for internal dev (checking runtime checks are catching) but
// since this is "fail fast" can be used to discover all offending TUs
// once.
// ∙ checks that math errno has been turned off. This is an anti-feature
// that should have been disabled by default decades ago. IEEE-754 won.
// This is like (by default) assuming bytes aren't 8-bits or that signed
// numbers aren't 2s-complement. Disabled by default for MSVC since
// it doesn't provide an option (other than fastmath equiv)
// ∙ GCC/clang: -fno-math-errno
// ∙ compile time & runtime
// ∙ disable both with FP_CC_ERRNO_SKIP
// ∙ checks that automatic fusing is disabled.
// ∙ GCC/clang: -ffp-contract=off
// ∙ floating point exceptions: check disabled by default
// ∙ to warn if disabled: #define FP_CC_EXCEPTIONS 0
// to warn if enable: #define FP_CC_EXCEPTIONS 1
// ∙ GCC/clang: -fno-trapping-math (disable)
// GCC/clang: -ftrapping-math (enable)
// ∙ compile time
// ∙ finite math
// ∙ compile time & runtime
// ∙ associative transforms
// ∙ compile time & runtime
// ∙ flushing denormals
// ∙ runtime
// ∙ reciprocals
// ∙ runtime
// ∙ respect infinities
// ∙ respect NaNs
// ∙ respect signed zeroes
//
#pragma once
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <math.h>
#include <errno.h>
//────────────────────────────────────────────────────────────────────────
// per translation unit: compile time checks
// ∙ allow disabling compile time checks: mostly for internal development
// (verify that runtime checks are catching bad options)
#if !defined(FP_CC_COMPILE_TIME_DISABLED)
// float point exceptions (sticky bits)
// Most people most of the time aren't going to care about this.
// ∙ by default: ignored
// ∙ if FP_CC_EXCEPTIONS is defined then:
// 0 = want disabled (error if enabled)
// 1 = want enabled (error if disabled)
#if defined(FP_CC_EXCEPTIONS)
#if (FP_CC_EXCEPTIONS)
static_assert((math_errhandling & MATH_ERREXCEPT)!=0, "error: floating exceptions disabled");
#else
static_assert((math_errhandling & MATH_ERREXCEPT)==0, "error: floating exceptions enabled");
#endif
#endif
#if defined(__GNUC__)
#if !defined(FP_CC_ERRNO_SKIP)
// Visual C doesn't provided an isolated disable (just with fast math)
static_assert((math_errhandling & MATH_ERRNO)==0, "error: math errno enabled");
#endif
#if defined(__FAST_MATH__)
static_assert(0, "error: -ffast-math is disallowed");
#endif
#if defined(__ASSOCIATIVE_MATH__)
static_assert(0, "error: -fassociative-math is disallowed");
#endif
#if (defined(__FINITE_MATH_ONLY__) && (__FINITE_MATH_ONLY__))
static_assert(0, "error: -ffinite-math-only is disallowed");
#endif
#elif defined(_MSC_VER)
//────────────────────────────────────────────────────────────────────────
// Microsoft compiler specific. I need to re-examine fp:precise and fp:strict
// WRT which macro are defined.
#if defined(_M_FP_CONTRACT)
static_assert(0, "error: /fp:contract is disallowed");
#endif
#if defined(_M_FP_FAST)
static_assert(0, "error: /fp:fast is disallowed");
#endif
#else
static_assert(0, "compiler needs some defines");
#endif
#else
//────────────────────────────────────────────────────────────────────────
// anything other than GCC/clang (compatiable) and MSC would go here
#endif
//────────────────────────────────────────────────────────────────────────
// runtime checks
#if defined(_MSC_VER)
#define FP_CC_FUNC __declspec(noinline)
#define fp_cc_retain(A,B)
#elif defined(__GNUC__)
// • used & retain "should" be unnecessary. they are to handle the case
// where the function optimizes to trival (returns an input or constant)
// with both GCC and clang allow to "inline", the function is then
// is unreachable and not emitted. (dev aid for disasm inspection)
#define FP_CC_FUNC __attribute__((noinline,used,retain))
// this attempts to prevent an inline and elimiation of a function that
// reduces to trival (another dev aid as per unused & retain above)
#if 0
#define fp_cc_retain(A,B) do { __asm__ ("" : "+x" (A), "+x"(B)); } while (0)
#else
#define fp_cc_retain(A,B)
#endif
#else
static_assert(0, "needs compiler define for noinline");
#endif
// "user" aid function: more verbose output from runtime checks
#if defined(FP_CC_VERBOSE)
#include <stdio.h>
#define FP_CC_DUMP(...) fprintf(stderr, __VA_ARGS__)
#else
#define FP_CC_DUMP(...)
#endif
// math errno is a curse that should have died decades ago.
// biggest concern in sqrt & sqrtf which can lower into a
// single hardware op. Visual-C adds insult to injury by not
// providing a seperate option for errno nor intrinsic sqrts
// for scalars w/o errno. Gotta roll your own.
#if !defined(FP_CC_ERRNO_SKIP)
#if defined(__GNUC__) && !defined(__FAST_MATH__)
#if !defined(__clang__) && !defined(__OPTIMIZE__)
// GCC (as of 14.2.0) ignores -fno-math-errno at -O0
#define FP_CC_ERRNO_SKIP
#endif
#elif defined(_MSC_VER)
#define FP_CC_ERRNO_SKIP
#endif
#ifndef FP_CC_SQRTF
#define FP_CC_SQRTF sqrtf
#endif
#endif
static inline uint32_t fp_cc_to_bits(float x)
{
uint32_t u; memcpy(&u, &x, 4); return u;
}
static inline float fp_cc_from_bits(uint32_t x)
{
float f; memcpy(&f, &x, 4); return f;
}
// noinline since parameters are (mostly) constants so we must disallow
// optimizing the test away (see macro note above).
// note: the body of each must be sufficent to perform the undesired
// optimization.
static float FP_CC_FUNC fp_cc_sqrt(float x)
{
fp_cc_retain(x,x);
return FP_CC_SQRTF(x);
}
static float FP_CC_FUNC fp_cc_add(float a, float b)
{
fp_cc_retain(a,b);
return a+b;
}
static float FP_CC_FUNC fp_cc_div_pi(float a)
{
fp_cc_retain(a,a);
return a/0x1.921fb6p1f;
}
static float FP_CC_FUNC fp_cc_mms(float a, float b, float c, float d)
{
fp_cc_retain(a,b);
return a*b-c*d;
}
static float FP_CC_FUNC fp_cc_fast_add_error(float a, float b)
{
fp_cc_retain(a,b);
return b-((a+b)-a);
}
static int FP_CC_FUNC fp_cc_lt_inf(float a)
{
static const double inf = (float)(0x1.0p+1000);
fp_cc_retain(a,a);
return a < inf;
}
static int FP_CC_FUNC fp_cc_is_ordered(float a)
{
fp_cc_retain(a,a);
return a == a;
}
static float FP_CC_FUNC fp_cc_add_zero(float a)
{
fp_cc_retain(a,a);
return a+0.0f;
}
//────────────────────────────────────────────────────────────────────────
// per translation unit: runtime checks
// meh: user has to tell the including file
#ifndef FP_CC_FILE
#define FP_CC_FILE "(unknown)"
#endif
// runtime check if math is setting errno by checking sqrtf
// (or provided replacement: looking at visual-c here)
// ∙ GCC, clang: "should" never trigger since it's covered by a compile time check
// ∙ MSC: going to get a compile time and run time warnings unless a
// replacement has been provided or check is disabled.
static uint32_t FP_CC_FUNC fp_cc_check_errno(uint32_t e)
{
#if !defined(FP_CC_ERRNO_SKIP)
errno = 0;
(void)fp_cc_sqrt(-1.f);
if (errno == 0) return e;
fprintf(stderr, "\n warning: sqrt setting errno");
return e+1;
#else
return e;
#endif
}
// correct result is zero helper
static uint32_t FP_CC_FUNC fp_cc_check_zero(float r, char* msg)
{
if (r == 0.f) return 0;
fprintf(stderr, "\n error: %s", msg);
return 1;
}
// correct result is non-zero helper
static uint32_t FP_CC_FUNC fp_cc_check_not_zero(float r, char* msg)
{
if (!(r == 0.f)) return 0;
fprintf(stderr, "\n error: %s", msg);
return 1;
}
// add min_denorm to itself returns a denormal. if "flush on load" or "flush on result" is enabled
// then result will be zero. Notice can only catch if the control word has already be modified
// at the time of execution.
//
// NOTE: a false postive can occur if the compiler converts fp_cc_min_denormal to zero
static uint32_t FP_CC_FUNC fp_cc_check_daz(uint32_t e)
{
// (a,b) are bit patterns of min_denormal and 2*min_denormal
uint32_t a = 1;
uint32_t b = a+1;
float f;
fp_cc_retain(b,a);
// attempt to move a denormal into a register w/o a floating point operation
// (frown..not useful as is)
f = fp_cc_from_bits(b);
e += fp_cc_check_not_zero(fp_cc_add(f,f), "denormals flushing to zero");
return e;
}
static uint32_t FP_CC_FUNC fp_cc_check_associative(uint32_t e)
{
static const float a = 1.f;
static const float b = 0x1.6a09e6p-40f;
float r = fp_cc_fast_add_error(a,b);
e += fp_cc_check_not_zero(r, "associative transform");
return e;
}
static uint32_t FP_CC_FUNC fp_cc_check_fused(uint32_t e)
{
// compute: ab-cd with ab==cd and both a and b are odd so ab != RN(ab)
// without fusing result is zero. both possible fused version RN(ab,-RN(ab)) & RN(RN(ab)-ab)
// return a non-zero result.
static const float a = 0x1.6a09e6p-1f;
static const float b = 0x1.6a09e6p-1f; // just let b=a as well
static const float w = 0x1.b3f548p-27f;
float r = fp_cc_mms(a,b,a,b);
if (r == 0.f) return e;
fprintf(stderr, "\n error: %s", "compiler fused to FMA");
r = fabsf(r);
if (r != w)
fprintf(stderr, "\n unexpected answer: %a expected %a with fusing", r, w);
return e+1;
}
static uint32_t FP_CC_FUNC fp_cc_check_reciprocal(uint32_t e)
{
// divide 'x' by pi. should get 'c' if transformed we get 'w'
// RN(x/RN(pi)) = c & RN(x * RN(1/RN(pi))) = w
static const float x = 0x1.000012p+0f;
static const float c = 0x1.45f31ep-2f;
static const float w = 0x1.45f31cp-2f;
float r = fp_cc_div_pi(x);
if (r == c) return e;
if (r == w)
fprintf(stderr, "\n error: %s", "reciprocal math enabled");
else
fprintf(stderr, "\n error: %s", "reciprocal math unexpected result!!");
return e+1;
}
static uint32_t FP_CC_FUNC fp_cc_check_infinites(uint32_t e)
{
static const float a = 0x1.fffffep+127f;
if (fp_cc_lt_inf(a+a) == 0)
return e;
fprintf(stderr, "\n error: %s", "ignoring infinities");
return e + 1;
}
static uint32_t FP_CC_FUNC fp_cc_check_nan(uint32_t e)
{
static const float nan = 0.f/0.f;
if (fp_cc_is_ordered(nan) == 0)
return e;
fprintf(stderr, "\n error: %s", "ignoring NaNs");
return e + 1;
}
static uint32_t FP_CC_FUNC fp_cc_check_signed_zeroes(uint32_t e)
{
float a = fp_cc_from_bits(0x80000000);
float r = fp_cc_add_zero(a);
uint32_t t = fp_cc_to_bits(r);
if (t == 0) return e;
fprintf(stderr, "\n error: %s", "ignoring signed zeroes");
return e + 1;
}
//────────────────────────────────────────────────────────────────────────
// run all of the checks
static void fp_compiler_check(void)
{
uint32_t errors = 0;
FP_CC_DUMP("fp_compiler_check: %-50s ", FP_CC_FILE);
errors = fp_cc_check_errno(errors);
errors = fp_cc_check_daz(errors);
errors = fp_cc_check_fused(errors);
errors = fp_cc_check_associative(errors);
errors = fp_cc_check_reciprocal(errors);
errors = fp_cc_check_infinites(errors);
errors = fp_cc_check_nan(errors);
errors = fp_cc_check_signed_zeroes(errors);
if (errors != 0)
fprintf(stderr, "\n float point options violations: %u\n", errors);
else {
FP_CC_DUMP(" done\n");
}
}
//────────────────────────────────────────────────────────────────────────
// crowbar calling 'fp_compiler_check' at init time per TU
#if defined(__GNUC__)
// execute the test as late as possible
static void __attribute__((constructor(65535))) fp_compiler_check_root(void) { fp_compiler_check(); }
#elif defined(_MSC_VER)
static void const (* const p_test_init)(void) = fp_compiler_check;
#pragma section(".CRT$XCU", read)
__declspec(allocate(".CRT$XCU")) static void (*p_test_init_section)(void) = fp_compiler_check;
#endif
#undef FP_CC_FUNC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment